/*
 * Copyright (C) 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.hardware.display;

import android.annotation.FloatRange;
import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.annotation.TestApi;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.MathUtils;

import com.android.internal.util.XmlUtils;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;

import java.io.IOException;

/**
 * BrightnessCorrection encapsulates a correction to the brightness, without comitting to the
 * actual correction scheme.
 * It is used by the BrightnessConfiguration, which maps context (e.g. the foreground app's package
 * name and category) to corrections that need to be applied to the brightness within that context.
 * Corrections are currently done by the app that has the top activity of the focused stack, either
 * by its package name, or (if its package name is not mapped to any correction) by its category.
 *
 * @hide
 */
@SystemApi
@TestApi
public final class BrightnessCorrection implements Parcelable {

    private static final int SCALE_AND_TRANSLATE_LOG = 1;

    private static final String TAG_SCALE_AND_TRANSLATE_LOG = "scale-and-translate-log";

    private BrightnessCorrectionImplementation mImplementation;

    // Parcelable classes must be final, and protected methods are not allowed in APIs, so we can't
    // make this class abstract and use composition instead of inheritence.
    private BrightnessCorrection(BrightnessCorrectionImplementation implementation) {
        mImplementation = implementation;
    }

    /**
     * Creates a BrightnessCorrection that given {@code brightness}, corrects it to be
     * {@code exp(scale * ln(brightness) + translate)}.
     *
     * @param scale
     *      How much to scale the log (base e) brightness.
     * @param translate
     *      How much to translate the log (base e) brightness.
     *
     * @return A BrightnessCorrection that given {@code brightness}, corrects it to be
     * {@code exp(scale * ln(brightness) + translate)}.
     *
     * @throws IllegalArgumentException
     *      - scale or translate are NaN.
     */
    @NonNull
    public static BrightnessCorrection createScaleAndTranslateLog(float scale, float translate) {
        BrightnessCorrectionImplementation implementation =
                new ScaleAndTranslateLog(scale, translate);
        return new BrightnessCorrection(implementation);
    }

    /**
     * Applies the brightness correction to a given brightness.
     *
     * @param brightness
     *      The brightness.
     *
     * @return The corrected brightness.
     */
    @FloatRange(from = 0.0)
    public float apply(@FloatRange(from = 0.0) float brightness) {
        return mImplementation.apply(brightness);
    }

    /**
     * Returns a string representation.
     *
     * @return A string representation.
     */
    public String toString() {
        return mImplementation.toString();
    }


    @Override
    public boolean equals(Object o) {
        if (o == this) {
            return true;
        }
        if (!(o instanceof BrightnessCorrection)) {
            return false;
        }
        BrightnessCorrection other = (BrightnessCorrection) o;
        return other.mImplementation.equals(mImplementation);
    }

    @Override
    public int hashCode() {
        return mImplementation.hashCode();
    }

    public static final @android.annotation.NonNull Creator<BrightnessCorrection> CREATOR =
            new Creator<BrightnessCorrection>() {
                public BrightnessCorrection createFromParcel(Parcel in) {
                    final int type = in.readInt();
                    switch (type) {
                        case SCALE_AND_TRANSLATE_LOG:
                            return ScaleAndTranslateLog.readFromParcel(in);
                    }
                    return null;
                }

                public BrightnessCorrection[] newArray(int size) {
                    return new BrightnessCorrection[size];
                }
            };

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        mImplementation.writeToParcel(dest);
    }

    @Override
    public int describeContents() {
        return 0;
    }

    /**
     * Writes the correction to an XML serializer.
     *
     * @param serializer
     *      The XML serializer.
     *
     * @hide
     */
    public void saveToXml(XmlSerializer serializer) throws IOException {
        mImplementation.saveToXml(serializer);
    }

    /**
     * Read a correction from an XML parser.
     *
     * @param parser
     *      The XML parser.
     *
     * @throws IOException
     *      The parser failed to read the XML file.
     * @throws XmlPullParserException
     *      The parser failed to parse the XML file.
     *
     * @hide
     */
    public static BrightnessCorrection loadFromXml(XmlPullParser parser) throws IOException,
            XmlPullParserException {
        final int depth = parser.getDepth();
        while (XmlUtils.nextElementWithin(parser, depth)) {
            if (TAG_SCALE_AND_TRANSLATE_LOG.equals(parser.getName())) {
                return ScaleAndTranslateLog.loadFromXml(parser);
            }
        }
        return null;
    }

    private static float loadFloatFromXml(XmlPullParser parser, String attribute) {
        final String string = parser.getAttributeValue(null, attribute);
        try {
            return Float.parseFloat(string);
        } catch (NullPointerException | NumberFormatException e) {
            return Float.NaN;
        }
    }

    private interface BrightnessCorrectionImplementation {
        float apply(float brightness);
        String toString();
        void writeToParcel(Parcel dest);
        void saveToXml(XmlSerializer serializer) throws IOException;
        // Package-private static methods:
        // static BrightnessCorrection readFromParcel(Parcel in);
        // static BrightnessCorrection loadFromXml(XmlPullParser parser) throws IOException,
        //      XmlPullParserException;
    }

    /**
     * A BrightnessCorrection that given {@code brightness}, corrects it to be
     * {@code exp(scale * ln(brightness) + translate)}.
     */
    private static class ScaleAndTranslateLog implements BrightnessCorrectionImplementation {
        private static final float MIN_SCALE = 0.5f;
        private static final float MAX_SCALE = 2.0f;
        private static final float MIN_TRANSLATE = -0.6f;
        private static final float MAX_TRANSLATE = 0.7f;

        private static final String ATTR_SCALE = "scale";
        private static final String ATTR_TRANSLATE = "translate";

        private final float mScale;
        private final float mTranslate;

        ScaleAndTranslateLog(float scale, float translate) {
            if (Float.isNaN(scale) || Float.isNaN(translate)) {
                throw new IllegalArgumentException("scale and translate must be numbers");
            }
            mScale = MathUtils.constrain(scale, MIN_SCALE, MAX_SCALE);
            mTranslate = MathUtils.constrain(translate, MIN_TRANSLATE, MAX_TRANSLATE);
        }

        @Override
        public float apply(float brightness) {
            return MathUtils.exp(mScale * MathUtils.log(brightness) + mTranslate);
        }

        @Override
        public String toString() {
            return "ScaleAndTranslateLog(" + mScale + ", " + mTranslate + ")";
        }

        @Override
        public boolean equals(Object o) {
            if (o == this) {
                return true;
            }
            if (!(o instanceof ScaleAndTranslateLog)) {
                return false;
            }
            ScaleAndTranslateLog other = (ScaleAndTranslateLog) o;
            return other.mScale == mScale && other.mTranslate == mTranslate;
        }

        @Override
        public int hashCode() {
            int result = 1;
            result = result * 31 + Float.hashCode(mScale);
            result = result * 31 + Float.hashCode(mTranslate);
            return result;
        }

        @Override
        public void writeToParcel(Parcel dest) {
            dest.writeInt(SCALE_AND_TRANSLATE_LOG);
            dest.writeFloat(mScale);
            dest.writeFloat(mTranslate);
        }

        @Override
        public void saveToXml(XmlSerializer serializer) throws IOException {
            serializer.startTag(null, TAG_SCALE_AND_TRANSLATE_LOG);
            serializer.attribute(null, ATTR_SCALE, Float.toString(mScale));
            serializer.attribute(null, ATTR_TRANSLATE, Float.toString(mTranslate));
            serializer.endTag(null, TAG_SCALE_AND_TRANSLATE_LOG);
        }

        static BrightnessCorrection readFromParcel(Parcel in) {
            float scale = in.readFloat();
            float translate = in.readFloat();
            return BrightnessCorrection.createScaleAndTranslateLog(scale, translate);
        }

        static BrightnessCorrection loadFromXml(XmlPullParser parser) throws IOException,
                XmlPullParserException {
            final float scale = loadFloatFromXml(parser, ATTR_SCALE);
            final float translate = loadFloatFromXml(parser, ATTR_TRANSLATE);
            return BrightnessCorrection.createScaleAndTranslateLog(scale, translate);
        }
    }
}
