package com.openxc.measurements;

import com.google.common.base.Objects;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;

import android.content.Context;
import android.util.Log;

import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.openxc.NoValueException;
import com.openxc.messages.EventedSimpleVehicleMessage;
import com.openxc.messages.MessageKey;
import com.openxc.messages.NamedVehicleMessage;
import com.openxc.messages.SimpleVehicleMessage;
import com.openxc.units.Unit;
import com.openxc.util.AgingData;
import com.openxc.util.Range;

/**
 * The BaseMeasurement is the base implementation of the Measurement, and
 * wraps an instance of a {@link Unit}, and the value it returns is always
 * in terms of this Unit.
 *
 * The Unit wrapper might seem annoying at first, but it is critical to avoid
 * misinterpreting the unit and crashing your lander into Mars
 * (http://en.wikipedia.org/wiki/Mars_Climate_Orbiter).
 *
 * Most applications will not use this class directly, but will import specific
 * child classes that correspond to specific types of measurements - i.e. the
 * parameterized instances of this class with a Unit. That may seem like a
 * "psuedo-typedef" but we're using it to enforce the binding between
 * the measurement and its unit type. This unfortunately means we have to add
 * constructors to every child class because they aren't inherited from
 * Measurement. If you know of a better way, please say so.
 */
public abstract class BaseMeasurement<TheUnit extends Unit>
            implements Measurement {
    private static final String TAG = BaseMeasurement.class.toString();

    protected AgingData<TheUnit> mValue;
    private Range<TheUnit> mRange;
    private static Map<Class<? extends Measurement>, String>
            sCachedPrettyNames = new HashMap<>();
    private static BiMap<String, Class<? extends Measurement>>
            sMeasurementIdToClass;

    static {
        sMeasurementIdToClass = HashBiMap.create();
        try {
            cacheMeasurementId(AcceleratorPedalPosition.class);
            cacheMeasurementId(BrakePedalStatus.class);
            cacheMeasurementId(EngineSpeed.class);
            cacheMeasurementId(FuelConsumed.class);
            cacheMeasurementId(FuelLevel.class);
            cacheMeasurementId(HeadlampStatus.class);
            cacheMeasurementId(HighBeamStatus.class);
            cacheMeasurementId(IgnitionStatus.class);
            cacheMeasurementId(Latitude.class);
            cacheMeasurementId(Longitude.class);
            cacheMeasurementId(Odometer.class);
            cacheMeasurementId(ParkingBrakeStatus.class);
            cacheMeasurementId(SteeringWheelAngle.class);
            cacheMeasurementId(TorqueAtTransmission.class);
            cacheMeasurementId(TransmissionGearPosition.class);
            cacheMeasurementId(TurnSignalStatus.class);
            cacheMeasurementId(VehicleButtonEvent.class);
            cacheMeasurementId(VehicleDoorStatus.class);
            cacheMeasurementId(VehicleSpeed.class);
            cacheMeasurementId(WindshieldWiperStatus.class);
            
            // added by Jon
            cacheMeasurementId(LateralAccel.class);
            cacheMeasurementId(LongAccel.class);
            cacheMeasurementId(YawRate.class);
            cacheMeasurementId(Altitude.class);
            cacheMeasurementId(WheelSpeedFL.class);
            cacheMeasurementId(WheelSpeedFR.class);
            cacheMeasurementId(WheelSpeedRL.class);
            cacheMeasurementId(WheelSpeedRR.class);
            cacheMeasurementId(ACIndicator.class); // Added by Bibhrajit
        
        } catch(UnrecognizedMeasurementTypeException e) { }
    }

    public abstract String getGenericName();

    /**
     * Construct a new Measurement with the given value.
     *
     * @param value the TheUnit this measurement represents.
     */
    public BaseMeasurement(TheUnit value) {
        if(!sMeasurementIdToClass.inverse().containsKey(this.getClass())) {
            try {
                cacheMeasurementId(this.getClass());
            } catch(UnrecognizedMeasurementTypeException e) {
                Log.w(TAG, "Incomplete BaseMeasurement subclass", e);
            }
        }
        mValue = new AgingData<TheUnit>(value);
    }

    /**
     * Construct an new Measurement with the given value and valid Range.
     *
     * There is not currently any automated verification that the value is
     * within the range - this is up to the application programmer.
     *
     * @param value the TheUnit this measurement represents.
     * @param range the valid {@link Range} of values for this measurement.
     */
    public BaseMeasurement(TheUnit value, Range<TheUnit> range) {
        this(value);
        mRange = range;
    }

    @Override
    public void setTimestamp(long timestamp) {
        mValue.setTimestamp(timestamp);
    }

    @Override
    public long getAge() {
        return mValue.getAge();
    }

    @Override
    public long getBirthtime() {
        return mValue.getTimestamp();
    }

    @Override
    public boolean hasRange() {
        return mRange != null;
    }

    @Override
    public Range<TheUnit> getRange() {
        return mRange;
    }

    @Override
    public TheUnit getValue() {
        return mValue.getValue();
    }

    @Override
    public Object getSerializedValue() {
        return getValue().getSerializedValue();
    }

    @Override
    public SimpleVehicleMessage toVehicleMessage() {
        return new SimpleVehicleMessage(mValue.getTimestamp(),
                getGenericName(), getSerializedValue());
    }

    public String getName(Context context) {
        String name = getGenericName();
        if(!sCachedPrettyNames.containsKey(getClass())) {
            // Make sure to not use the package name here, we have to find the
            // resource using the package name of the app using the library instead.
            int identifier = context.getResources().getIdentifier(
                    getGenericName() + "_label", "string", context.getPackageName());
            if(identifier != 0) {
                name = context.getString(identifier);
                sCachedPrettyNames.put(getClass(), name);
            }
        } else if(sCachedPrettyNames.get(getClass()) != null) {
            name = sCachedPrettyNames.get(getClass());
        }
        return name;
    }

    private static void cacheMeasurementId(
            Class<? extends Measurement> measurementType)
            throws UnrecognizedMeasurementTypeException {
        String measurementId;
        try {
            measurementId = (String) measurementType.getField("ID").get(
                    measurementType);
            sMeasurementIdToClass.put(measurementId, measurementType);
        } catch(NoSuchFieldException e) {
            throw new UnrecognizedMeasurementTypeException(
                    measurementType + " doesn't have an ID field", e);
        } catch(IllegalAccessException e) {
            throw new UnrecognizedMeasurementTypeException(
                    measurementType + " has an inaccessible " +
                    "ID field", e);
        }
    }

    public static MessageKey getKeyForMeasurement(
            Class<? extends Measurement> measurementType)
            throws UnrecognizedMeasurementTypeException {
        if(!sMeasurementIdToClass.inverse().containsKey(measurementType)) {
            cacheMeasurementId(measurementType);
        }
        return new NamedVehicleMessage(
                sMeasurementIdToClass.inverse().get(measurementType)).getKey();
    }

    public static Class<? extends Measurement>
            getClassForId(String measurementId)
            throws UnrecognizedMeasurementTypeException {
        Class<? extends Measurement> result = sMeasurementIdToClass.get(
                measurementId);
        if(result == null) {
            throw new UnrecognizedMeasurementTypeException(
                    "Didn't have a measurement with ID " + measurementId +
                    " cached");
        }
        return result;
    }

    public static Measurement getMeasurementFromMessage(
            SimpleVehicleMessage message)
                throws UnrecognizedMeasurementTypeException, NoValueException {
        Class<? extends Measurement> measurementClass =
            BaseMeasurement.getClassForId(message.getName());
        return BaseMeasurement.getMeasurementFromMessage(measurementClass,
                message);
    }

    public static Measurement getMeasurementFromMessage(
            Class<? extends Measurement> measurementType,
            SimpleVehicleMessage message)
                throws UnrecognizedMeasurementTypeException, NoValueException {
        Constructor<? extends Measurement> constructor = null;
        if(message == null) {
            throw new NoValueException();
        }

        try {
            Measurement measurement = null;
            SimpleVehicleMessage simpleMessage = message.asSimpleMessage();
            Class<?> valueClass = simpleMessage.getValue().getClass();
            if(valueClass == Double.class || valueClass == Integer.class) {
                valueClass = Number.class;
            }

            if(message instanceof EventedSimpleVehicleMessage) {
                EventedSimpleVehicleMessage eventedMessage =
                        message.asEventedMessage();

                Class<?> eventClass = eventedMessage.getEvent().getClass();
                if(eventClass == Double.class || eventClass == Integer.class) {
                    eventClass = Number.class;
                }

                try {
                    constructor = measurementType.getConstructor(
                            valueClass, eventClass);
                } catch(NoSuchMethodException e) {
                    throw new UnrecognizedMeasurementTypeException(
                            measurementType +
                            " doesn't have the expected constructor, " +
                           measurementType + "(" +
                           valueClass + ", " + eventClass + ")");
                }

                measurement = constructor.newInstance(
                        eventedMessage.getValue(),
                        eventedMessage.getEvent());
            } else {
                try {
                    constructor = measurementType.getConstructor(valueClass);
                } catch(NoSuchMethodException e) {
                    throw new UnrecognizedMeasurementTypeException(
                            measurementType +
                            " doesn't have the expected constructor, " +
                           measurementType + "(" +
                           valueClass + ")");
                }

                measurement = constructor.newInstance(
                        simpleMessage.getValue());
            }

            if (simpleMessage.getTimestamp() != null) {
                measurement.setTimestamp(simpleMessage.getTimestamp());
            }
            // https://github.com/openxc/openxc-android/issues/185
            return measurement;
        } catch(InstantiationException e) {
            throw new UnrecognizedMeasurementTypeException(
                    measurementType + " is abstract", e);
        } catch(IllegalAccessException e) {
            throw new UnrecognizedMeasurementTypeException(
                    measurementType + " has a private constructor", e);
        } catch(IllegalArgumentException e) {
            throw new UnrecognizedMeasurementTypeException(
                    measurementType + " has unexpected arguments", e);
        } catch(InvocationTargetException e) {
            throw new UnrecognizedMeasurementTypeException(
                    measurementType + "'s constructor threw an exception",
                    e);
        }
    }

    @Override
    public boolean equals(Object obj) {
        if(this == obj) {
            return true;
        }

        if(obj == null) {
            return false;
        }

        if(getClass() != obj.getClass()) {
            return false;
        }

        @SuppressWarnings("unchecked")
        final BaseMeasurement<TheUnit> other = (BaseMeasurement<TheUnit>) obj;
        return Objects.equal(getValue(), other.getValue()) &&
            Objects.equal(other.getRange(), getRange());
    }

    @Override
    public String toString() {
        return getValue().toString();
    }
}
