import { Interaction, Component } from "models";
import { pond } from "protobuf-ts/pond";
import { quack } from "protobuf-ts/quack";
import { or, notNull } from "utils/types";
import { sameComponentID, emptyComponentId, componentIDToString } from "pbHelpers/Component";
import moment, { weekdaysShort } from "moment-timezone";
import { describeMeasurement } from "./MeasurementDescriber";
import { capitalize } from "lodash";

export function getDefaultInteraction(): Interaction {
  let interaction = new Interaction();
  interaction.settings.schedule = {
    weekdays: ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"],
    timeOfDayStart: "00:00",
    timeOfDayEnd: "24:00",
    timezone: moment.tz.guess()
  } as pond.InteractionSchedule;
  interaction.settings.result = {
    type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_REPORT
  } as pond.InteractionResult;
  return interaction;
}

export function isSource(componentID: quack.IComponentID, interaction: Interaction): boolean {
  return interaction.settings.source
    ? sameComponentID(componentID, interaction.settings.source)
    : false;
}

export function isSink(componentID: quack.IComponentID, interaction: Interaction): boolean {
  return interaction.settings.sink
    ? sameComponentID(componentID, interaction.settings.sink)
    : false;
}

export function getInteractionID(interaction: pond.IInteractionSettings): string {
  const { source, sink } = interaction;
  let sourceID = componentIDToString(or(source, {} as quack.IComponentID));
  let sinkID = componentIDToString(or(sink, {} as quack.IComponentID));
  return sourceID + ":" + sinkID + ":" + interaction.instance;
}

export function findInteractionsAsSource(
  component: quack.IComponentID,
  interactions: Interaction[]
): Interaction[] {
  return interactions.filter(i => isSource(component, i));
}

export function findInteractionsAsSink(
  component: quack.IComponentID,
  interactions: Interaction[]
): Interaction[] {
  return interactions.filter(i => isSink(component, i));
}

export function findInteractions(
  component: quack.IComponentID,
  interactions: Interaction[]
): Interaction[] {
  return interactions.filter(i => isSource(component, i) || isSink(component, i));
}

export function HasInteraction(
  component: quack.IComponentID,
  interactions: Interaction[]
): boolean {
  if (!component || !interactions) {
    return false;
  }

  let hasInteraction = false;
  interactions.forEach(i => {
    if (isSource(component, i) || isSink(component, i)) {
      hasInteraction = true;
    }
  });

  return hasInteraction;
}

export function timeOfDayDescriptor(schedule: pond.IInteractionSchedule) {
  let timeOfDayStart = "00:00";
  let timeOfDayEnd = "24:00";
  if (schedule) {
    if (schedule.timeOfDayStart) {
      timeOfDayStart = schedule.timeOfDayStart;
    }

    if (schedule.timeOfDayEnd) {
      timeOfDayEnd = schedule.timeOfDayEnd;
    }
  }

  if (timeOfDayStart === "00:00" && timeOfDayEnd === "24:00") {
    return "allDay";
  } else if (timeOfDayStart === "00:00") {
    return "before";
  } else if (timeOfDayEnd === "24:00") {
    return "after";
  } else {
    return "from";
  }
}

export function friendlyTimeOfDayDescriptor(
  descriptor: "allDay" | "before" | "after" | "from"
): string {
  switch (descriptor) {
    case "allDay":
      return "all day";
    default:
      return descriptor;
  }
}

const isWeekend = (day: string): boolean => {
  return day.toLowerCase().startsWith("sat") || day.toLowerCase().startsWith("sun");
};

export function abbreviatedWeekdays(schedule: pond.IInteractionSchedule): string {
  if (!schedule || !schedule.weekdays) {
    return "";
  }

  let weekdays = schedule.weekdays;
  let numDays = weekdays.length;

  if (numDays === 1) {
    return capitalize(weekdays[0]);
  }

  if (numDays === 2 && weekdays.every(day => isWeekend(day))) {
    return "Weekends";
  }

  if (numDays === 5 && weekdays.every(day => !isWeekend(day))) {
    return "Weekdays";
  }

  if (numDays === 7) {
    return "Everyday";
  }

  return weekdaysShort()
    .filter(abrev => weekdays.find(day => day.toLowerCase().startsWith(abrev.toLowerCase())))
    .join(", ");
}

export const interactionResultText = (interaction: Interaction, sink?: Component) => {
  let sinkName = sink ? sink.name() : "Unknown";
  let result = interaction.settings.result;
  let notifications = interaction.settings.notifications;
  if (!result) {
    return "Unknown when";
  }
  let value = result.value ? "on" : "off";
  let notify = notifications && notifications.notify;
  switch (result.type) {
    case quack.InteractionResultType.INTERACTION_RESULT_TYPE_REPORT:
      return (notify ? "Send notifications" : "Reporting measurement") + " when";
    case quack.InteractionResultType.INTERACTION_RESULT_TYPE_RUN:
      return "Run " + sinkName + " when";
    case quack.InteractionResultType.INTERACTION_RESULT_TYPE_SET:
      return "Set " + sinkName + " to " + value + " when";
    case quack.InteractionResultType.INTERACTION_RESULT_TYPE_TOGGLE:
      return "Toggle " + sinkName + " " + value + " when";
  }
  return "Unknown when";
};

export const interactionConditionText = (
  source: Component | undefined,
  condition: pond.IInteractionCondition,
  and: boolean
) => {
  let comparison = "is exactly";
  switch (condition.comparison) {
    case quack.RelationalOperator.RELATIONAL_OPERATOR_EQUAL_TO:
      comparison = "is exactly";
      break;
    case quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN:
      comparison = "is above";
      break;
    case quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN:
      comparison = "is below";
      break;
  }
  if (!condition.measurementType) return "Unknown " + comparison + " " + condition.value;
  let sourceType = source && source.settings.type;
  let sourceSubtype = source && source.settings.subtype;
  let measurement = describeMeasurement(condition.measurementType, sourceType, sourceSubtype);
  return (
    (and ? "and " : "") +
    measurement.label() +
    " " +
    comparison +
    " " +
    measurement.convertWithUnits(condition.value ? condition.value : 0)
  );
};

const keyTranslator = new Map<keyof pond.InteractionSettings, string>([]);

// calibration_offset
// grain_type
// grain_filled_to

// Keys will be stringified by default if not found in the keyTranslator
export function TranslateKey(key: keyof pond.InteractionSettings): string {
  let translatedKey = keyTranslator.get(key);
  return translatedKey ? translatedKey : capitalize(key.toString());
}

const getComparisons = (conditions: pond.InteractionCondition[]) => {
  let ret = " ";
  conditions.forEach(cond => {
    switch (cond.measurementType) {
      case quack.MeasurementType.MEASUREMENT_TYPE_BOOLEAN:
        ret = ret + "Bool";
        break;
      case quack.MeasurementType.MEASUREMENT_TYPE_ANALOG:
        ret = ret + "Analog";
        break;
      case quack.MeasurementType.MEASUREMENT_TYPE_CAPACITANCE:
        ret = ret + "Capacitance";
        break;
      case quack.MeasurementType.MEASUREMENT_TYPE_CONDUCTIVITY:
        ret = ret + "Conductivity";
        break;
      case quack.MeasurementType.MEASUREMENT_TYPE_DISTANCE_CM:
        ret = ret + "Distance(cm)";
        break;
      case quack.MeasurementType.MEASUREMENT_TYPE_LIGHT:
        ret = ret + "Light";
        break;
      case quack.MeasurementType.MEASUREMENT_TYPE_ORP:
        ret = ret + "ORP";
        break;
      case quack.MeasurementType.MEASUREMENT_TYPE_PERCENT:
        ret = ret + "Percent";
        break;
      case quack.MeasurementType.MEASUREMENT_TYPE_PH:
        ret = ret + "PH";
        break;
      case quack.MeasurementType.MEASUREMENT_TYPE_PPB:
        ret = ret + "PPB";
        break;
      case quack.MeasurementType.MEASUREMENT_TYPE_PPM:
        ret = ret + "PPM";
        break;
      case quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE:
        ret = ret + "Pressure";
        break;
      case quack.MeasurementType.MEASUREMENT_TYPE_RAIN:
        ret = ret + "Rain";
        break;
      case quack.MeasurementType.MEASUREMENT_TYPE_RSSI:
        ret = ret + "RSSI";
        break;
      case quack.MeasurementType.MEASUREMENT_TYPE_SPEED:
        ret = ret + "Speed";
        break;
      case quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE:
        ret = ret + "Temperature";
        break;
      case quack.MeasurementType.MEASUREMENT_TYPE_VOLTAGE:
        ret = ret + "Voltage";
        break;
      case quack.MeasurementType.MEASUREMENT_TYPE_WEIGHT:
        ret = ret + "Weight";
        break;
      default:
        ret = "Invalid Measurement Type";
        break;
    }
    switch (cond.comparison) {
      case quack.RelationalOperator.RELATIONAL_OPERATOR_EQUAL_TO:
        ret = ret + " equal to " + cond.value;
        break;
      case quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN:
        ret = ret + " greater than " + cond.value;
        break;
      case quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN:
        ret = ret + " less than " + cond.value;
        break;
      default:
        ret = ret + " Invalid Operator";
    }
  });
  return ret;
};

const resultTypeString = (result: pond.InteractionResult | null | undefined) => {
  let ret = "No Result Found";
  if (result) {
    switch (result.type) {
      case quack.InteractionResultType.INTERACTION_RESULT_TYPE_REPORT:
        ret = "Report";
        break;
      case quack.InteractionResultType.INTERACTION_RESULT_TYPE_RUN:
        ret = "Run";
        break;
      case quack.InteractionResultType.INTERACTION_RESULT_TYPE_SET:
        ret = "Set";
        break;
      case quack.InteractionResultType.INTERACTION_RESULT_TYPE_TOGGLE:
        ret = "Toggle";
        break;
      default:
        ret = "Invalid";
    }
  }
  return ret;
};

// might not need this
const valueTranslator = new Map<
  keyof pond.InteractionSettings,
  (interaction: pond.InteractionSettings) => string
>([
  [
    "source",
    interaction =>
      or(
        interaction.source?.type.toString() +
          "-" +
          interaction.source?.addressType.toString() +
          "-" +
          interaction.source?.address.toString(),
        "Failed to Find Source"
      )
  ],
  ["conditions", interaction => or(getComparisons(interaction.conditions), "")],
  ["result", interaction => or(resultTypeString(interaction.result), "")],
  ["notifications", interaction => (interaction.notifications?.notify ? "True" : "False")]
  // ["schedule", interaction => "s"]
]);

// Values will be stringified by default if its key is not found in the valueTranslator
export function TranslateValue(
  key: keyof pond.InteractionSettings,
  interaction: pond.InteractionSettings
) {
  let translatorFunc = valueTranslator.get(key);
  let value: any = or(interaction[key], "");
  let d: string;
  d = or(value.toString(), "");
  return translatorFunc ? translatorFunc(interaction) : interaction[key] ? capitalize(d) : "";
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~VALIDATORS~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

export function isInteractionValid(interaction: Interaction) {
  return (
    notNull(interaction.settings) &&
    isSourceValid(or(interaction.settings.source, {} as quack.ComponentID)) &&
    isSinkValid(or(interaction.settings.sink, {} as quack.ComponentID)) &&
    areConditionsValid(interaction.settings.conditions) &&
    isResultValid(or(interaction.settings.result, {} as pond.InteractionResult)) &&
    isNotificationsValid(
      or(interaction.settings.notifications, {} as pond.InteractionNotifications)
    )
  );
}

export function isSourceValid(source: quack.IComponentID | null | undefined) {
  return source && JSON.stringify(source) !== JSON.stringify(emptyComponentId());
}

export function isSinkValid(sink: quack.IComponentID) {
  return notNull(sink);
}

export function areConditionsValid(conditions: Array<pond.IInteractionCondition>) {
  let conditionsValid = true;
  conditions.forEach((condition: pond.IInteractionCondition) => {
    if (!isConditionValid(condition)) {
      conditionsValid = false;
    }
  });

  return conditionsValid;
}

export function isConditionValid(condition: pond.IInteractionCondition) {
  if (!notNull(condition)) {
    return false;
  }

  return (
    isMeasurementTypeValid(
      or(condition.measurementType, quack.MeasurementType.MEASUREMENT_TYPE_INVALID)
    ) &&
    isComparisonValid(
      or(condition.comparison, quack.RelationalOperator.RELATIONAL_OPERATOR_INVALID)
    ) &&
    isValueValid(or(condition.value, null))
  );
}

export function isMeasurementTypeValid(measurementType: quack.MeasurementType) {
  return (
    notNull(measurementType) && measurementType !== quack.MeasurementType.MEASUREMENT_TYPE_INVALID
  );
}

export function isComparisonValid(comparison: quack.RelationalOperator) {
  return notNull(comparison) && comparison !== quack.RelationalOperator.RELATIONAL_OPERATOR_INVALID;
}

export function isValueValid(value: number) {
  return notNull(value) && !isNaN(value);
}

export function isResultValid(result: pond.IInteractionResult) {
  return (
    notNull(result) &&
    isResultTypeValid(
      or(result.type, quack.InteractionResultType.INTERACTION_RESULT_TYPE_INVALID)
    ) &&
    or(result.dutyCycle, 0) >= 0
  );
}

export function isResultTypeValid(resultType: quack.InteractionResultType) {
  return (
    notNull(resultType) &&
    resultType !== quack.InteractionResultType.INTERACTION_RESULT_TYPE_INVALID
  );
}

export function isNotificationsValid(notifications: pond.IInteractionNotifications) {
  return notNull(notifications);
}
