import { cloneDeep } from "lodash";
import { pond } from "protobuf-ts/pond";
import { quack } from "protobuf-ts/quack";
import { User } from "models";
import moment from "moment";
import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
import { or } from "utils";

export interface MeasurementsFor {
  type: quack.MeasurementType;
  label: string;
  values: number[];
  unit: string;
  colour: string;
  error: boolean;
}

export interface convertedUnitMeasurement {
  timestamp: string;
  measurementsFor: MeasurementsFor[];
}

export class UnitMeasurement {
  public componentId: string = "";
  public type: quack.MeasurementType = 0;
  public label: string = "";
  public values: pond.ValueArray[] = [];
  public timestamps: string[] = [];
  public colour: string = "";
  public unit: string = "";

  public static create(pb?: pond.UnitMeasurementsForComponent, user?: User): UnitMeasurement {
    let my = new UnitMeasurement();
    if (pb) {
      let describer = describeMeasurement(pb.type, pb.componentType);
      let values = pb.values;
      if (user) {
        values = unitConversion(pb.values, pb.type, user);
      }
      my.componentId = pb.componentId;
      my.type = pb.type;
      my.values = values;
      my.timestamps = pb.timestamps;
      my.colour = describer.colour();
      my.unit = describer.GetUnit();
      my.label = describer.label();
    }
    return my;
  }

  public static clone(other?: UnitMeasurement): UnitMeasurement {
    if (other) {
      return UnitMeasurement.create(
        pond.UnitMeasurementsForComponent.fromObject(
          cloneDeep({
            componentId: other.componentId,
            label: other.label,
            type: other.type,
            values: other.values,
            timestamps: other.timestamps,
            colour: other.colour,
            unit: other.unit
          })
        )
      );
    }
    return UnitMeasurement.create();
  }

  public static any(data: any, user?: User): UnitMeasurement {
    let my = UnitMeasurement.create(
      pond.UnitMeasurementsForComponent.fromObject(cloneDeep(data)),
      user
    );
    return my;
  }

  /**
   * takes in the unitMeasurementsForComponents from list and re-structures them into an array of objects that have the timestamp
   * and all measurements at that timestamp in one object
   * the converted measurement is meant to be used with the measurement summary
   * @param toConvert
   * @returns
   */
  public static convertMeasurements(toConvert: UnitMeasurement[]): convertedUnitMeasurement[] {
    let map: Map<string, convertedUnitMeasurement> = new Map<string, convertedUnitMeasurement>();
    let converted: convertedUnitMeasurement[] = [];
    toConvert.forEach((um: UnitMeasurement) => {
      um.values.forEach((vals, i) => {
        let current = map.get(um.timestamps[i]);
        //checks if there is a converted measurement with that timestamp yet, if there is add the new one to it
        if (current) {
          current.measurementsFor.push({
            type: um.type,
            label: um.label,
            values: vals.values,
            colour: um.colour,
            unit: um.unit,
            error: vals.error
          });
          // if there is not then create a new one and put it in the map
        } else {
          let converted: convertedUnitMeasurement = {
            timestamp: um.timestamps[i],
            measurementsFor: [
              {
                type: um.type,
                label: um.label,
                values: vals.values,
                colour: um.colour,
                unit: um.unit,
                error: vals.error
              }
            ]
          };
          map.set(um.timestamps[i], converted);
        }
      });
    });
    converted = Array.from(map.values());
    return converted;
  }

  //similar to the above however it only returns the converted measurement with the most recent timestamp
  public static convertLastMeasurement(toConvert: UnitMeasurement[]): convertedUnitMeasurement {
    let converted: convertedUnitMeasurement = {
      measurementsFor: [],
      timestamp: ""
    };
    toConvert.forEach((um: UnitMeasurement) => {
      if (um.values.length > 0 && um.timestamps.length > 0) {
        let recentPos = 0;
        let timeVal = 0;
        um.timestamps.forEach((timestamp, pos) => {
          if (moment(timestamp).valueOf() > timeVal) {
            recentPos = pos;
            timeVal = moment(timestamp).valueOf();
          }
        });
        converted.timestamp = um.timestamps[recentPos];
        converted.measurementsFor.push({
          type: um.type,
          label: um.label,
          values: um.values[recentPos].values,
          colour: um.colour,
          unit: um.unit,
          error: um.values[recentPos].error
        });
      }
    });
    return converted;
  }

  /**
   * will return a converted unit measurement where the values in the the measurementsFor array only contain the number for the requested node,
   * if the requested node is outside the array (component doesn't have that node) the array will simply be empty
   * @param toConvert
   * @param node
   * @returns
   */
  public static convertLastMeasurementByNode(
    toConvert: UnitMeasurement[],
    node: number
  ): convertedUnitMeasurement {
    let converted: convertedUnitMeasurement = {
      measurementsFor: [],
      timestamp: ""
    };
    toConvert.forEach((um: UnitMeasurement) => {
      if (um.values.length > 0 && um.timestamps.length > 0) {
        let recentPos = 0;
        let timeVal = 0;
        um.timestamps.forEach((timestamp, pos) => {
          if (moment(timestamp).valueOf() > timeVal) {
            recentPos = pos;
            timeVal = moment(timestamp).valueOf();
          }
        });
        converted.timestamp = um.timestamps[recentPos];
        converted.measurementsFor.push({
          type: um.type,
          label: um.label,
          values: or([um.values[recentPos].values[node]], []),
          colour: um.colour,
          unit: um.unit,
          error: um.values[recentPos].error
        });
      }
    });
    return converted;
  }

  public getRecentForNode(node: number): number | undefined {
    if (this.values.length > 0) {
      if (this.values[this.values.length - 1].values[node]) {
        return this.values[this.values.length - 1].values[node];
      }
    }
    return undefined;
  }
}

/**
 * for handling the conversion of values based on user preferences, this is run for each unit measurement when it is created from the data from protobuf
 * @param values
 * @param type
 * @param user
 * @returns
 */
function unitConversion(
  values: pond.ValueArray[],
  type: quack.MeasurementType,
  user: User
): pond.ValueArray[] {
  let newVals = values;
  if (
    type === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE &&
    user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT
  ) {
    newVals.forEach((val, i) => {
      val.values.forEach((v, j) => {
        newVals[i].values[j] = Math.round((v * 1.8 + 32) * 100) / 100;
      });
    });
  }
  if (type === quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE) {
    if (user.settings.pressureUnit === pond.PressureUnit.PRESSURE_UNIT_KILOPASCALS) {
      newVals.forEach((val, i) => {
        val.values.forEach((v, j) => {
          newVals[i].values[j] = v / 1000;
        });
      });
      //} else if (user.settings.pressureUnit === pond.PressureUnit.PRESSURE_UNIT_INCHES_OF_WATER) {
    } else {
      //by default if there is nothing set use iwg
      newVals.forEach((val, i) => {
        val.values.forEach((v, j) => {
          newVals[i].values[j] = Math.round((v / 248.8) * 100) / 100;
        });
      });
    }
  }
  if (type === quack.MeasurementType.MEASUREMENT_TYPE_DISTANCE_CM) {
    if (user.settings.distanceUnit === pond.DistanceUnit.DISTANCE_UNIT_FEET) {
      newVals.forEach((val, i) => {
        val.values.forEach((v, j) => {
          newVals[i].values[j] = Math.round((v / 30.48) * 100) / 100;
        });
      });
    } else {
      //use meters as the default for users that have none set yet
      newVals.forEach((val, i) => {
        val.values.forEach((v, j) => {
          newVals[i].values[j] = v / 100;
        });
      });
    }
  }
  if (type === quack.MeasurementType.MEASUREMENT_TYPE_SPEED) {
    if (user.settings.distanceUnit === pond.DistanceUnit.DISTANCE_UNIT_FEET) {
      newVals.forEach((val, i) => {
        val.values.forEach((v, j) => {
          //convert m/s to ft/m by multiplying by 196.9
          newVals[i].values[j] = Math.round((v * 196.9)*100) /100
        });
      });
    }
  }
  return newVals;
}
