/*
 * Copyright (C) 2016 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.car.hardware;

import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.BOILERPLATE_CODE;

import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.car.VehicleAreaType;
import android.car.VehicleAreaType.VehicleAreaTypeValue;
import android.car.VehiclePropertyType;
import android.car.annotation.AddedInOrBefore;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.SparseArray;

import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * Represents general information about car property such as data type and min/max ranges for car
 * areas (if applicable). This class supposed to be immutable, parcelable and could be passed over.
 *
 * @param <T> refer to Parcel#writeValue(Object) to get a list of all supported types. The class
 * should be visible to framework as default class loader is being used here.
 *
 */
public final class CarPropertyConfig<T> implements Parcelable {
    private final int mAccess;
    private final int mAreaType;
    private final int mChangeMode;
    private final ArrayList<Integer> mConfigArray;
    private final String mConfigString;
    private final float mMaxSampleRate;
    private final float mMinSampleRate;
    private final int mPropertyId;
    private final SparseArray<AreaConfig<T>> mSupportedAreas;
    private final Class<T> mType;

    private CarPropertyConfig(int access, int areaType, int changeMode,
            ArrayList<Integer> configArray, String configString,
            float maxSampleRate, float minSampleRate, int propertyId,
            SparseArray<AreaConfig<T>> supportedAreas, Class<T> type) {
        mAccess = access;
        mAreaType = areaType;
        mChangeMode = changeMode;
        mConfigArray = configArray;
        mConfigString = configString;
        mMaxSampleRate = maxSampleRate;
        mMinSampleRate = minSampleRate;
        mPropertyId = propertyId;
        mSupportedAreas = supportedAreas;
        mType = type;
    }

    /** @hide */
    @IntDef(prefix = {"VEHICLE_PROPERTY_ACCESS"}, value = {
        VEHICLE_PROPERTY_ACCESS_NONE,
        VEHICLE_PROPERTY_ACCESS_READ,
        VEHICLE_PROPERTY_ACCESS_WRITE,
        VEHICLE_PROPERTY_ACCESS_READ_WRITE
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface VehiclePropertyAccessType {}

    /** Property Access Unknown */
    @AddedInOrBefore(majorVersion = 33)
    public static final int VEHICLE_PROPERTY_ACCESS_NONE = 0;
    /** The property is readable */
    @AddedInOrBefore(majorVersion = 33)
    public static final int VEHICLE_PROPERTY_ACCESS_READ = 1;
    /** The property is writable */
    @AddedInOrBefore(majorVersion = 33)
    public static final int VEHICLE_PROPERTY_ACCESS_WRITE = 2;
    /** The property is readable and writable */
    @AddedInOrBefore(majorVersion = 33)
    public static final int VEHICLE_PROPERTY_ACCESS_READ_WRITE = 3;

    /** @hide */
    @IntDef(prefix = {"VEHICLE_PROPERTY_CHANGE_MODE"}, value = {
        VEHICLE_PROPERTY_CHANGE_MODE_STATIC,
        VEHICLE_PROPERTY_CHANGE_MODE_ONCHANGE,
        VEHICLE_PROPERTY_CHANGE_MODE_CONTINUOUS,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface VehiclePropertyChangeModeType {}

    /** Properties of this type must never be changed. */
    @AddedInOrBefore(majorVersion = 33)
    public static final int VEHICLE_PROPERTY_CHANGE_MODE_STATIC = 0;
    /** Properties of this type must report when there is a change. */
    @AddedInOrBefore(majorVersion = 33)
    public static final int VEHICLE_PROPERTY_CHANGE_MODE_ONCHANGE = 1;
    /** Properties of this type change continuously. */
    @AddedInOrBefore(majorVersion = 33)
    public static final int VEHICLE_PROPERTY_CHANGE_MODE_CONTINUOUS = 2;

    /**
     * Return the access type of the car property.
     * <p>The access type could be one of the following:
     * <ul>
     *   <li>{@link CarPropertyConfig#VEHICLE_PROPERTY_ACCESS_NONE}</li>
     *   <li>{@link CarPropertyConfig#VEHICLE_PROPERTY_ACCESS_READ}</li>
     *   <li>{@link CarPropertyConfig#VEHICLE_PROPERTY_ACCESS_WRITE}</li>
     *   <li>{@link CarPropertyConfig#VEHICLE_PROPERTY_ACCESS_READ_WRITE}</li>
     * </ul>
     *
     * @return the access type of the car property.
     */
    @AddedInOrBefore(majorVersion = 33)
    public @VehiclePropertyAccessType int getAccess() {
        return mAccess;
    }

    /**
     * Return the area type of the car property.
     * <p>The area type could be one of the following:
     * <ul>
     *   <li>{@link VehicleAreaType#VEHICLE_AREA_TYPE_GLOBAL}</li>
     *   <li>{@link VehicleAreaType#VEHICLE_AREA_TYPE_WINDOW}</li>
     *   <li>{@link VehicleAreaType#VEHICLE_AREA_TYPE_SEAT}</li>
     *   <li>{@link VehicleAreaType#VEHICLE_AREA_TYPE_DOOR}</li>
     *   <li>{@link VehicleAreaType#VEHICLE_AREA_TYPE_MIRROR}</li>
     *   <li>{@link VehicleAreaType#VEHICLE_AREA_TYPE_WHEEL}</li>
     * </ul>
     *
     * @return the area type of the car property.
     */
    @AddedInOrBefore(majorVersion = 33)
    public @VehicleAreaTypeValue int getAreaType() {
        return mAreaType;
    }

    /**
     * Return the change mode of the car property.
     *
     * <p>The change mode could be one of the following:
     * <ul>
     *   <li>{@link CarPropertyConfig#VEHICLE_PROPERTY_CHANGE_MODE_STATIC }</li>
     *   <li>{@link CarPropertyConfig#VEHICLE_PROPERTY_CHANGE_MODE_ONCHANGE}</li>
     *   <li>{@link CarPropertyConfig#VEHICLE_PROPERTY_CHANGE_MODE_CONTINUOUS}</li>
     * </ul>
     *
     * @return the change mode of properties.
     */
    @AddedInOrBefore(majorVersion = 33)
    public @VehiclePropertyChangeModeType int getChangeMode() {
        return mChangeMode;
    }

    /**
     *
     * @return Additional configuration parameters. For different properties, configArrays have
     * different information.
     */
    @NonNull
    @AddedInOrBefore(majorVersion = 33)
    public List<Integer> getConfigArray() {
        return Collections.unmodifiableList(mConfigArray);
    }

    /**
     *
     * @return Some properties may require additional information passed over this
     * string. Most properties do not need to set this.
     * @hide
     */
    @AddedInOrBefore(majorVersion = 33)
    public String getConfigString() {
        return mConfigString;
    }

    /**
     *
     * @return Max sample rate in Hz. Must be defined for VehiclePropertyChangeMode::CONTINUOUS
     * return 0 if change mode is not continuous.
     */
    @AddedInOrBefore(majorVersion = 33)
    public float getMaxSampleRate() {
        return mMaxSampleRate;
    }

    /**
     *
     * @return Min sample rate in Hz.Must be defined for VehiclePropertyChangeMode::CONTINUOUS
     * return 0 if change mode is not continuous.
     */
    @AddedInOrBefore(majorVersion = 33)
    public float getMinSampleRate() {
        return mMinSampleRate;
    }

    /**
     * @return Property identifier, must be one of enums in
     *   {@link android.car.VehiclePropertyIds}.
     */
    @AddedInOrBefore(majorVersion = 33)
    public int getPropertyId() {
        return mPropertyId;
    }

    /**
     * Returns the value type of the vehicle property.
     * <p>The value type could be one of the following:
     * <ul>
     *   <li>Boolean</li>
     *   <li>Float</li>
     *   <li>Float[]</li>
     *   <li>Integer</li>
     *   <li>Integer[]</li>
     *   <li>Long</li>
     *   <li>Long[]</li>
     *   <li>String</li>
     *   <li>byte[]</li>
     *   <li>Object[]</li>
     * </ul>
     *
     * @return the value type of the vehicle property.
     */
    @NonNull
    @AddedInOrBefore(majorVersion = 33)
    public Class<T> getPropertyType() {
        return mType;
    }

    /**
     *
     * @return true if this property doesn't hold car area-specific configuration.
     */
    @AddedInOrBefore(majorVersion = 33)
    public boolean isGlobalProperty() {
        return mAreaType == VehicleAreaType.VEHICLE_AREA_TYPE_GLOBAL;
    }

    /**
     *
     * @return the number of areaIds for properties.
     * @hide
     */
    @AddedInOrBefore(majorVersion = 33)
    public int getAreaCount() {
        return mSupportedAreas.size();
    }

    /**
     *
     * @return Array of areaIds. An AreaID is a combination of one or more areas,
     * and is represented using a bitmask of Area enums. Different AreaTypes may
     * not be mixed in a single AreaID. For instance, a window area cannot be
     * combined with a seat area in an AreaID.
     * Rules for mapping a zoned property to AreaIDs:
     *  - A property must be mapped to an array of AreaIDs that are impacted when
     *    the property value changes.
     *  - Each element in the array must represent an AreaID, in which, the
     *    property value can only be changed together in all the areas within
     *    an AreaID and never independently. That is, when the property value
     *    changes in one of the areas in an AreaID in the array, then it must
     *    automatically change in all other areas in the AreaID.
     *  - The property value must be independently controllable in any two
     *    different AreaIDs in the array.
     *  - An area must only appear once in the array of AreaIDs. That is, an
     *    area must only be part of a single AreaID in the array.
     */
    @NonNull
    @AddedInOrBefore(majorVersion = 33)
    public int[] getAreaIds() {
        int[] areaIds = new int[mSupportedAreas.size()];
        for (int i = 0; i < areaIds.length; i++) {
            areaIds[i] = mSupportedAreas.keyAt(i);
        }
        return areaIds;
    }

    /**
     * @return  the first areaId.
     * Throws {@link IllegalStateException} if supported area count not equals to one.
     * @hide
     */
    @AddedInOrBefore(majorVersion = 33)
    public int getFirstAndOnlyAreaId() {
        if (mSupportedAreas.size() != 1) {
            throw new IllegalStateException("Expected one and only area in this property. Prop: 0x"
                    + Integer.toHexString(mPropertyId));
        }
        return mSupportedAreas.keyAt(0);
    }

    /**
     *
     * @param areaId
     * @return true if areaId is existing.
     * @hide
     */
    @AddedInOrBefore(majorVersion = 33)
    public boolean hasArea(int areaId) {
        return mSupportedAreas.indexOfKey(areaId) >= 0;
    }

    /**
     *
     * @param areaId
     * @return Min value in given areaId. Null if not have min value in given area.
     */
    @Nullable
    @AddedInOrBefore(majorVersion = 33)
    public T getMinValue(int areaId) {
        AreaConfig<T> area = mSupportedAreas.get(areaId);
        return area == null ? null : area.getMinValue();
    }

    /**
     *
     * @param areaId
     * @return Max value in given areaId. Null if not have max value in given area.
     */
    @Nullable
    @AddedInOrBefore(majorVersion = 33)
    public T getMaxValue(int areaId) {
        AreaConfig<T> area = mSupportedAreas.get(areaId);
        return area == null ? null : area.getMaxValue();
    }

    /**
     *
     * @return Min value in areaId 0. Null if not have min value.
     */
    @Nullable
    @AddedInOrBefore(majorVersion = 33)
    public T getMinValue() {
        AreaConfig<T> area = mSupportedAreas.get(0);
        return area == null ? null : area.getMinValue();
    }

    /**
     *
     * @return Max value in areaId 0. Null if not have max value.
     */
    @Nullable
    @AddedInOrBefore(majorVersion = 33)
    public T getMaxValue() {
        AreaConfig<T> area = mSupportedAreas.get(0);
        return area == null ? null : area.getMaxValue();
    }

    @Override
    @ExcludeFromCodeCoverageGeneratedReport(reason = BOILERPLATE_CODE)
    @AddedInOrBefore(majorVersion = 33)
    public int describeContents() {
        return 0;
    }

    @Override
    @AddedInOrBefore(majorVersion = 33)
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(mAccess);
        dest.writeInt(mAreaType);
        dest.writeInt(mChangeMode);
        dest.writeInt(mConfigArray.size());
        for (int i = 0; i < mConfigArray.size(); i++) {
            dest.writeInt(mConfigArray.get(i));
        }
        dest.writeString(mConfigString);
        dest.writeFloat(mMaxSampleRate);
        dest.writeFloat(mMinSampleRate);
        dest.writeInt(mPropertyId);
        dest.writeInt(mSupportedAreas.size());
        for (int i = 0; i < mSupportedAreas.size(); i++) {
            dest.writeInt(mSupportedAreas.keyAt(i));
            dest.writeParcelable(mSupportedAreas.valueAt(i), flags);
        }
        dest.writeString(mType.getName());
    }

    @SuppressWarnings("unchecked")
    private CarPropertyConfig(Parcel in) {
        mAccess = in.readInt();
        mAreaType = in.readInt();
        mChangeMode = in.readInt();
        int configArraySize = in.readInt();
        mConfigArray = new ArrayList<Integer>(configArraySize);
        for (int i = 0; i < configArraySize; i++) {
            mConfigArray.add(in.readInt());
        }
        mConfigString = in.readString();
        mMaxSampleRate = in.readFloat();
        mMinSampleRate = in.readFloat();
        mPropertyId = in.readInt();
        int areaSize = in.readInt();
        mSupportedAreas = new SparseArray<>(areaSize);
        for (int i = 0; i < areaSize; i++) {
            int areaId = in.readInt();
            AreaConfig<T> area = in.readParcelable(getClass().getClassLoader());
            mSupportedAreas.put(areaId, area);
        }
        String className = in.readString();
        try {
            mType = (Class<T>) Class.forName(className);
        } catch (ClassNotFoundException e) {
            throw new IllegalArgumentException("Class not found: " + className);
        }
    }

    @AddedInOrBefore(majorVersion = 33)
    public static final Creator<CarPropertyConfig> CREATOR = new Creator<CarPropertyConfig>() {
        @Override
        public CarPropertyConfig createFromParcel(Parcel in) {
            return new CarPropertyConfig(in);
        }

        @Override
        public CarPropertyConfig[] newArray(int size) {
            return new CarPropertyConfig[size];
        }
    };

    /** @hide */
    @Override
    @AddedInOrBefore(majorVersion = 33)
    public String toString() {
        return "CarPropertyConfig{"
                + "mPropertyId=" + mPropertyId
                + ", mAccess=" + mAccess
                + ", mAreaType=" + mAreaType
                + ", mChangeMode=" + mChangeMode
                + ", mConfigArray=" + mConfigArray
                + ", mConfigString=" + mConfigString
                + ", mMaxSampleRate=" + mMaxSampleRate
                + ", mMinSampleRate=" + mMinSampleRate
                + ", mSupportedAreas=" + mSupportedAreas
                + ", mType=" + mType
                + '}';
    }

    /**
     * Represents min/max value of car property.
     * @param <T>
     * @hide
     */
    public static class AreaConfig<T> implements Parcelable {
        @Nullable private final T mMinValue;
        @Nullable private final T mMaxValue;

        private AreaConfig(T minValue, T maxValue) {
            mMinValue = minValue;
            mMaxValue = maxValue;
        }

        @AddedInOrBefore(majorVersion = 33)
        public static final Parcelable.Creator<AreaConfig<Object>> CREATOR =
                getCreator(Object.class);

        private static <E> Parcelable.Creator<AreaConfig<E>> getCreator(final Class<E> clazz) {
            return new Creator<AreaConfig<E>>() {
                @Override
                public AreaConfig<E> createFromParcel(Parcel source) {
                    return new AreaConfig<>(source);
                }

                @Override @SuppressWarnings("unchecked")
                public AreaConfig<E>[] newArray(int size) {
                    return (AreaConfig<E>[]) Array.newInstance(clazz, size);
                }
            };
        }

        @SuppressWarnings("unchecked")
        private AreaConfig(Parcel in) {
            mMinValue = (T) in.readValue(getClass().getClassLoader());
            mMaxValue = (T) in.readValue(getClass().getClassLoader());
        }

        @AddedInOrBefore(majorVersion = 33)
        @Nullable public T getMinValue() {
            return mMinValue;
        }

        @AddedInOrBefore(majorVersion = 33)
        @Nullable public T getMaxValue() {
            return mMaxValue;
        }

        @Override
        @AddedInOrBefore(majorVersion = 33)
        public int describeContents() {
            return 0;
        }

        @Override
        @AddedInOrBefore(majorVersion = 33)
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeValue(mMinValue);
            dest.writeValue(mMaxValue);
        }

        @Override
        @AddedInOrBefore(majorVersion = 33)
        public String toString() {
            return "CarAreaConfig{"
                    + "mMinValue=" + mMinValue
                    + ", mMaxValue=" + mMaxValue
                    + '}';
        }
    }

    /**
     * Prepare an instance of CarPropertyConfig
     *
     * @return Builder<T>
     * @hide
     */
    @SystemApi
    @AddedInOrBefore(majorVersion = 33)
    public static <T> Builder<T> newBuilder(Class<T> type, int propertyId, int areaType,
                                            int areaCapacity) {
        return new Builder<>(areaCapacity, areaType, propertyId, type);
    }


    /**
     * Prepare an instance of CarPropertyConfig
     *
     * @return Builder<T>
     * @hide
     */
    @AddedInOrBefore(majorVersion = 33)
    public static <T> Builder<T> newBuilder(Class<T> type, int propertyId, int areaType) {
        return new Builder<>(0, areaType, propertyId, type);
    }


    /**
     * @param <T>
     * @hide
     * */
    @SystemApi
    public static class Builder<T> {
        private int mAccess;
        private final int mAreaType;
        private int mChangeMode;
        private final ArrayList<Integer> mConfigArray;
        private String mConfigString;
        private float mMaxSampleRate;
        private float mMinSampleRate;
        private final int mPropertyId;
        private final SparseArray<AreaConfig<T>> mSupportedAreas;
        private final Class<T> mType;

        private Builder(int areaCapacity, int areaType, int propertyId, Class<T> type) {
            mAreaType = areaType;
            mConfigArray = new ArrayList<>();
            mPropertyId = propertyId;
            if (areaCapacity != 0) {
                mSupportedAreas = new SparseArray<>(areaCapacity);
            } else {
                mSupportedAreas = new SparseArray<>();
            }
            mType = type;
        }

        /**
         * Add supported areas parameter to CarPropertyConfig
         *
         * @return Builder<T>
         */
        @AddedInOrBefore(majorVersion = 33)
        public Builder<T> addAreas(int[] areaIds) {
            for (int id : areaIds) {
                mSupportedAreas.put(id, null);
            }
            return this;
        }

        /**
         * Add area to CarPropertyConfig
         *
         * @return Builder<T>
         */
        @AddedInOrBefore(majorVersion = 33)
        public Builder<T> addArea(int areaId) {
            return addAreaConfig(areaId, null, null);
        }

        /**
         * Add areaConfig to CarPropertyConfig
         *
         * @return Builder<T>
         */
        @AddedInOrBefore(majorVersion = 33)
        public Builder<T> addAreaConfig(int areaId, T min, T max) {
            if (!isRangeAvailable(min, max)) {
                mSupportedAreas.put(areaId, null);
            } else {
                mSupportedAreas.put(areaId, new AreaConfig<>(min, max));
            }
            return this;
        }

        /**
         * Set access parameter to CarPropertyConfig
         *
         * @return Builder<T>
         */
        @AddedInOrBefore(majorVersion = 33)
        public Builder<T> setAccess(int access) {
            mAccess = access;
            return this;
        }

        /**
         * Set changeMode parameter to CarPropertyConfig
         *
         * @return Builder<T>
         */
        @AddedInOrBefore(majorVersion = 33)
        public Builder<T> setChangeMode(int changeMode) {
            mChangeMode = changeMode;
            return this;
        }

        /**
         * Set configArray parameter to CarPropertyConfig
         *
         * @return Builder<T>
         */
        @AddedInOrBefore(majorVersion = 33)
        public Builder<T> setConfigArray(ArrayList<Integer> configArray) {
            mConfigArray.clear();
            mConfigArray.addAll(configArray);
            return this;
        }

        /**
         * Set configString parameter to CarPropertyConfig
         *
         * @return Builder<T>
         */
        @AddedInOrBefore(majorVersion = 33)
        public Builder<T> setConfigString(String configString) {
            mConfigString = configString;
            return this;
        }

        /**
         * Set maxSampleRate parameter to CarPropertyConfig
         *
         * @return Builder<T>
         */
        @AddedInOrBefore(majorVersion = 33)
        public Builder<T> setMaxSampleRate(float maxSampleRate) {
            mMaxSampleRate = maxSampleRate;
            return this;
        }

        /**
         * Set minSampleRate parameter to CarPropertyConfig
         *
         * @return Builder<T>
         */
        @AddedInOrBefore(majorVersion = 33)
        public Builder<T> setMinSampleRate(float minSampleRate) {
            mMinSampleRate = minSampleRate;
            return this;
        }

        /**
         * Builds a new {@link CarPropertyConfig}.
         */
        @AddedInOrBefore(majorVersion = 33)
        public CarPropertyConfig<T> build() {
            return new CarPropertyConfig<>(mAccess, mAreaType, mChangeMode, mConfigArray,
                                           mConfigString, mMaxSampleRate, mMinSampleRate,
                                           mPropertyId, mSupportedAreas, mType);
        }

        private boolean isRangeAvailable(T min, T max) {
            if (min == null || max == null) {
                return false;
            }
            int propertyType = mPropertyId & VehiclePropertyType.MASK;
            switch (propertyType) {
                case VehiclePropertyType.INT32:
                    return (Integer) min  != 0 || (Integer) max != 0;
                case VehiclePropertyType.INT64:
                    return (Long) min != 0L || (Long) max != 0L;
                case VehiclePropertyType.FLOAT:
                    return (Float) min != 0f || (Float) max != 0f;
                default:
                    return false;
            }
        }
    }
}
