import { numericToCalendar, calendarToNumeric, currentNumDate, currentCalDate } from "../util/dateHelpers";
import { defaultGeoResolution,
  defaultColorBy,
  defaultDateRange,
  defaultDistanceMeasure,
  defaultLayout,
  defaultFocus,
  controlsHiddenWidth,
  strainSymbol,
  twoColumnBreakpoint } from "../util/globals";
import * as types from "../actions/types";
import { calcBrowserDimensionsInitialState } from "./browserDimensions";
import { doesColorByHaveConfidence } from "../actions/recomputeReduxState";
import { hasMultipleGridPanels } from "../actions/panelDisplay";
import { Distance } from "../components/tree/phyloTree/types";
import { MeasurementsDisplay } from "./measurements/types";


export interface ColorScale {
  colorBy: string
  continuous: boolean
  domain?: unknown[]
  genotype: Genotype | null
  legendBounds?: LegendBounds
  legendLabels?: LegendLabels
  legendValues: LegendValues
  scale: (value: any) => string
  scaleType: ScaleType | null
  version: number
  visibleLegendValues: LegendValues
}

export interface Genotype {
  gene: string
  positions: number[]
  aa: boolean
}

export type Layout = "rect" | "radial" | "unrooted" | "clock" | "scatter"

export type Focus = "selected" | null

export type LegendBounds = {
  [key: string | number]: [number, number]
}

/** A map of legendValues to a value for display in the legend. */
export type LegendLabels = Map<unknown, unknown>

/** An array of values to display in the legend. */
// TODO: I think this should be number[] | string[] but that requires adding type guards
export type LegendValues = any[]

export type PerformanceFlags = Map<string, boolean>

export interface SelectedNode {
  existingFilterState: "active" | "inactive" | null
  idx: number
  isBranch: boolean
  name: string
  treeId: string
}

interface AvailableAPIData {
  datasets:
    {
      /** The URL path (sans preceding slash) to load the dataset */
      request: string

      /**
       * Does the dataset support snapshots (@YYYY-MM-DD)?
       */
      snapshots?: boolean

      /** v2 (unified) dataset JSON.
       * Present on the Auspice server, not present on nextstrain.org
       * Unused in Auspice client.
       */
      v2?: boolean

      /** a list of request paths which are candidates to be displayed as a second tree */
      secondTreeOptions: string[]

      /**
       * Defines the intended build URL (rendered in the byline) for the dataset.
       * This will be used if the actual dataset JSON doesn't define it itself.
       * Unused in Auspice server, used for community sources in nextstrain.org.
       */
      buildUrl?: null|string
    }[]
  narratives:
    {
      /** The URL path (sans preceding slash) to load the narrative */
      request: string
    }[]
}

export type ScaleType = "ordinal" | "categorical" | "continuous" | "temporal" | "boolean"

export interface ScatterVariables {
  showBranches?: boolean
  showRegression?: boolean
  x?: string
  xContinuous?: boolean
  xDomain?: number[]
  xTemporal?: boolean
  y?: string
  yContinuous?: boolean
  yDomain?: number[]
  yTemporal?: boolean
}

export interface TemporalConfidence {
  /**
   * Does the dataset include confidence values?
   */
  exists: boolean
  /**
   * Whether to display the toggle in the sidebar
   */
  display: boolean
  /**
   * Whether the confidence intervals are displayed (i.e. the toggle is on/off)
   */
  on: boolean
}

interface Defaults {
  distanceMeasure: Distance
  layout: Layout
  focus: Focus
  geoResolution: string
  filters: Record<string, unknown>
  filtersInFooter: string[]
  colorBy: string
  selectedBranchLabel: string
  tipLabelKey: string | symbol
  showTransmissionLines: boolean
  sidebarOpen?: boolean
}

export interface BasicControlsState {
  defaults: Defaults

  absoluteDateMax: string
  absoluteDateMaxNumeric: number
  absoluteDateMin: string
  absoluteDateMinNumeric: number
  analysisSlider: boolean
  animationPlayPauseButton: "Play" | "Pause"
  available?: AvailableAPIData
  branchLengthsToDisplay: string
  canRenderBranchLabels: boolean
  canTogglePanelLayout: boolean
  colorBy: string
  colorByConfidence: boolean
  coloringsPresentOnTree: Set<string>

  /** subset of coloringsPresentOnTree */
  coloringsPresentOnTreeWithConfidence: Set<string>

  colorScale?: ColorScale
  dateMax: string
  dateMaxNumeric: number
  dateMin: string
  dateMinNumeric: number
  distanceMeasure: Distance
  explodeAttr?: string
  filters: Record<string | symbol, Array<{ value: string, active: boolean }>>
  filtersInFooter: string[]
  focus: Focus
  geoResolution: string
  layout: Layout
  mapAnimationCumulative: boolean
  mapAnimationDurationInMilliseconds: number
  mapAnimationShouldLoop: boolean
  mapAnimationStartDate: unknown
  modal: 'download' | 'linkOut' | 'datasetSelector' | null
  normalizeFrequencies: boolean
  panelLayout: string
  panelsAvailable: string[]
  panelsToDisplay: string[]
  performanceFlags: PerformanceFlags
  quickdraw: boolean
  scatterVariables: ScatterVariables
  selectedBranchLabel: string
  selectedNode: SelectedNode | null
  showAllBranchLabels: boolean
  showOnlyPanels: boolean
  showTangle: boolean
  showStreamTrees: boolean
  streamTreeBranchLabel: string | null
  availableStreamLabelKeys: string[]
  showTransmissionLines: boolean
  showTreeToo: boolean
  sidebarOpen: boolean
  temporalConfidence: TemporalConfidence
  tipLabelKey: string | symbol
  zoomMax?: number
  zoomMin?: number
}

export interface MeasurementFilters {
  [key: string]: Map<string, {active: boolean}>
}
export interface MeasurementsControlState {
  measurementsGroupBy: string | undefined
  measurementsDisplay: MeasurementsDisplay | undefined
  measurementsShowOverallMean: boolean | undefined
  measurementsShowThreshold: boolean | undefined
  measurementsFilters: MeasurementFilters
  measurementsColorGrouping: string | undefined
}

export interface ControlsState extends BasicControlsState, MeasurementsControlState {}

/* defaultState is a fn so that we can re-create it
at any time, e.g. if we want to revert things (e.g. on dataset change)
*/
export const getDefaultControlsState = (): ControlsState => {
  const defaults: Defaults = {
    distanceMeasure: defaultDistanceMeasure,
    layout: defaultLayout,
    focus: defaultFocus,
    geoResolution: defaultGeoResolution,
    filters: {},
    filtersInFooter: [],
    colorBy: defaultColorBy,
    selectedBranchLabel: "none",
    tipLabelKey: strainSymbol,
    showTransmissionLines: true
  };
  // a default sidebarOpen status is only set via JSON, URL query
  // _or_ if certain URL keywords are triggered
  const initialSidebarState = getInitialSidebarState();
  if (initialSidebarState.setDefault) {
    defaults.sidebarOpen = initialSidebarState.sidebarOpen;
  }

  const dateMin = numericToCalendar(currentNumDate() - defaultDateRange);
  const dateMax = currentCalDate();
  const dateMinNumeric = calendarToNumeric(dateMin);
  const dateMaxNumeric = calendarToNumeric(dateMax);
  return {
    defaults,
    available: undefined,
    canTogglePanelLayout: true,
    temporalConfidence: { exists: false, display: false, on: false },
    layout: defaults.layout,
    scatterVariables: {},
    distanceMeasure: defaults.distanceMeasure,
    focus: defaults.focus,
    dateMin,
    dateMinNumeric,
    dateMax,
    dateMaxNumeric,
    absoluteDateMin: dateMin,
    absoluteDateMinNumeric: dateMinNumeric,
    absoluteDateMax: dateMax,
    absoluteDateMaxNumeric: dateMaxNumeric,
    colorBy: defaults.colorBy,
    colorByConfidence: false,
    colorScale: undefined,
    coloringsPresentOnTree: new Set(),
    coloringsPresentOnTreeWithConfidence: new Set(),
    explodeAttr: undefined,
    selectedBranchLabel: "none",
    showAllBranchLabels: false,
    selectedNode: null,
    canRenderBranchLabels: true,
    analysisSlider: false,
    geoResolution: defaults.geoResolution,
    filters: JSON.parse(JSON.stringify(defaults.filters)),
    filtersInFooter: JSON.parse(JSON.stringify(defaults.filtersInFooter)),
    modal: null,
    quickdraw: false, // if true, components may skip expensive computes.
    mapAnimationDurationInMilliseconds: 30000, // in milliseconds
    mapAnimationStartDate: null, // Null so it can pull the absoluteDateMin as the default
    mapAnimationCumulative: false,
    mapAnimationShouldLoop: false,
    animationPlayPauseButton: "Play",
    panelsAvailable: [],
    panelsToDisplay: [],
    panelLayout: calcBrowserDimensionsInitialState().width > twoColumnBreakpoint ? "grid" : "full",
    tipLabelKey: defaults.tipLabelKey,
    showTreeToo: false,
    showTangle: false,
    showStreamTrees: false,
    streamTreeBranchLabel: null,
    availableStreamLabelKeys: [],
    zoomMin: undefined,
    zoomMax: undefined,
    branchLengthsToDisplay: "divAndDate",
    sidebarOpen: initialSidebarState.sidebarOpen,
    showOnlyPanels: false,
    showTransmissionLines: true,
    normalizeFrequencies: true,
    measurementsGroupBy: undefined,
    measurementsDisplay: undefined,
    measurementsShowOverallMean: undefined,
    measurementsShowThreshold: undefined,
    measurementsColorGrouping: undefined,
    measurementsFilters: {},
    performanceFlags: new Map(),
  };
};

/**
 * Keeping measurements control state separate from getDefaultControlsState
 * in order to be able to differentiate when the page is loaded with and without
 * URL params for the measurements panel.
 *
 * The initial control state is constructed then the URL params update the state.
 * However, the measurements JSON is loaded after this, so it needs a way to
 * differentiate the clean slate vs the added URL params.
 */
export const defaultMeasurementsControlState: MeasurementsControlState = {
  measurementsGroupBy: undefined,
  measurementsDisplay: "mean",
  measurementsShowOverallMean: true,
  measurementsShowThreshold: true,
  measurementsFilters: {},
  measurementsColorGrouping: undefined,
};

/* while this may change, div currently doesn't have CIs, so they shouldn't be displayed. */
export const shouldDisplayTemporalConfidence = (exists, distMeasure, layout): boolean => exists && distMeasure === "num_date" && layout === "rect";

const Controls = (state: ControlsState = getDefaultControlsState(), action): ControlsState => {
  switch (action.type) {
    case types.URL_QUERY_CHANGE_WITH_COMPUTED_STATE: /* fallthrough */
    case types.CLEAN_START:
      return action.controls;
    case types.SET_AVAILABLE:
      return Object.assign({}, state, { available: action.data });
    case types.CHANGE_EXPLODE_ATTR:
      return Object.assign({}, state, {
        explodeAttr: action.explodeAttr,
        colorScale: Object.assign({}, state.colorScale, { visibleLegendValues: action.visibleLegendValues })
      });
    case types.CHANGE_BRANCH_LABEL:
      return Object.assign({}, state, { selectedBranchLabel: action.value });
    case types.TOGGLE_SHOW_ALL_BRANCH_LABELS:
      return Object.assign({}, state, { showAllBranchLabels: action.value });
    case types.CHANGE_LAYOUT:
      return Object.assign({}, state, {
        layout: action.layout,
        canRenderBranchLabels: action.canRenderBranchLabels,
        scatterVariables: action.scatterVariables,
        /* temporal confidence can only be displayed for rectangular trees */
        temporalConfidence: Object.assign({}, state.temporalConfidence, {
          display: shouldDisplayTemporalConfidence(
            state.temporalConfidence.exists,
            state.distanceMeasure,
            action.data
          ),
          on: false
        })
      });
    case types.CHANGE_DISTANCE_MEASURE: {
      const updatesToState: Partial<ControlsState> = {
        distanceMeasure: action.data,
        branchLengthsToDisplay: state.branchLengthsToDisplay
      };
      if (
        shouldDisplayTemporalConfidence(state.temporalConfidence.exists, action.data, state.layout)
      ) {
        updatesToState.temporalConfidence = Object.assign({}, state.temporalConfidence, {
          display: true
        });
      } else {
        updatesToState.temporalConfidence = Object.assign({}, state.temporalConfidence, {
          display: false,
          on: false
        });
      }
      return Object.assign({}, state, updatesToState);
    }
    case types.SET_FOCUS: {
      return {...state, focus: action.focus}
    }
    case types.CHANGE_DATES_VISIBILITY_THICKNESS: {
      const newDates: Partial<ControlsState> = { quickdraw: action.quickdraw };
      if (action.dateMin) {
        newDates.dateMin = action.dateMin;
        newDates.dateMinNumeric = action.dateMinNumeric;
      }
      if (action.dateMax) {
        newDates.dateMax = action.dateMax;
        newDates.dateMaxNumeric = action.dateMaxNumeric;
      }
      const colorScale = {...state.colorScale, visibleLegendValues: action.visibleLegendValues};
      return {...state, ...newDates, colorScale};
    }
    case types.CHANGE_ABSOLUTE_DATE_MIN:
      return Object.assign({}, state, {
        absoluteDateMin: action.data,
        absoluteDateMinNumeric: calendarToNumeric(action.data)
      });
    case types.CHANGE_ABSOLUTE_DATE_MAX:
      return Object.assign({}, state, {
        absoluteDateMax: action.data,
        absoluteDateMaxNumeric: calendarToNumeric(action.data)
      });
    case types.CHANGE_ANIMATION_TIME:
      return Object.assign({}, state, {
        mapAnimationDurationInMilliseconds: action.data
      });
    case types.CHANGE_ANIMATION_CUMULATIVE:
      return Object.assign({}, state, {
        mapAnimationCumulative: action.data
      });
    case types.CHANGE_ANIMATION_LOOP:
      return Object.assign({}, state, {
        mapAnimationShouldLoop: action.data
      });
    case types.MAP_ANIMATION_PLAY_PAUSE_BUTTON:
      return Object.assign({}, state, {
        quickdraw: action.data !== "Play",
        animationPlayPauseButton: action.data
      });
    case types.CHANGE_ANIMATION_START:
      return Object.assign({}, state, {
        mapAnimationStartDate: action.data
      });
    case types.CHANGE_PANEL_LAYOUT:
      return Object.assign({}, state, {
        panelLayout: action.data
      });
    case types.CHANGE_TIP_LABEL_KEY:
      return {...state, tipLabelKey: action.key};
    case types.TREE_TOO_DATA:
      return action.controls;
    case types.TOGGLE_PANEL_DISPLAY:
      return Object.assign({}, state, {
        panelsToDisplay: action.panelsToDisplay,
        panelLayout: action.panelLayout,
        canTogglePanelLayout: action.canTogglePanelLayout
      });
    case types.NEW_COLORS: {
      const newState = Object.assign({}, state, {
        colorBy: action.colorBy,
        colorScale: action.colorScale,
        colorByConfidence: doesColorByHaveConfidence(state, action.colorBy)
      });
      if (action.scatterVariables) {
        newState.scatterVariables = action.scatterVariables;
      }
      return newState;
    }
    case types.CHANGE_GEO_RESOLUTION:
      return Object.assign({}, state, {
        geoResolution: action.data
      });

    case types.SELECT_NODE: {
      /**
       * We don't store a (reference to) the node itself as that breaks redux's immutability checking,
       * instead we store the information needed to access it from the nodes array(s)
       */
      const existingFilterInfo = (state.filters?.[strainSymbol]||[]).find((info) => info.value===action.name);
      const existingFilterState = existingFilterInfo === undefined ? null :
        existingFilterInfo.active ? 'active' : 'inactive';
      const selectedNode: SelectedNode = {name: action.name, idx: action.idx, existingFilterState, isBranch: action.isBranch, treeId: action.treeId};
      return {...state, selectedNode};
    }
    case types.DESELECT_NODE: {
      return {...state, selectedNode: null}
    }
    case types.APPLY_FILTER: {
      // values arrive as array
      const filters = Object.assign({}, state.filters, {});
      if (action.values.length) { // set the filters to the new values
        filters[action.trait] = action.values;
      } else {                    // remove if no active+inactive filters
        delete filters[action.trait]
      }

      /**
       * If a tip modal is open then the strain will have been added as an active filter.
       * If we inactivate/remove that specific strain filter then we want to close the modal too!
       * (The inverse isn't true: filtering to a specific strain doesn't open the modal)
       */
      let selectedNode = state.selectedNode
      if (selectedNode &&
        !selectedNode.isBranch &&
        action.trait===strainSymbol &&
        !action.values.find((f) => f.value===selectedNode.name && f.active)
      ) {
        selectedNode = null;
      }
      return Object.assign({}, state, {
        filters,
        selectedNode,
      });
    }
    case types.TOGGLE_TEMPORAL_CONF:
      return Object.assign({}, state, {
        temporalConfidence: Object.assign({}, state.temporalConfidence, {
          on: !state.temporalConfidence.on
        })
      });
    case types.SET_MODAL:
      return Object.assign({}, state, {
        modal: action.modal || null
      });
    case types.REMOVE_TREE_TOO:
      return Object.assign({}, state, {
        showTreeToo: false,
        showTangle: false,
        canTogglePanelLayout: hasMultipleGridPanels(state.panelsAvailable),
        panelsToDisplay: state.panelsAvailable.slice()
      });
    case types.TOGGLE_TANGLE:
      if (state.showTreeToo) {
        return Object.assign({}, state, { showTangle: !state.showTangle });
      }
      return state;
    case types.TOGGLE_STREAM_TREE:
      return {...state, showStreamTrees: action.showStreamTrees};
    case types.CHANGE_STREAM_TREE_BRANCH_LABEL:
      return {...state, streamTreeBranchLabel: action.streamTreeBranchLabel, showStreamTrees: true};
    case types.TOGGLE_SIDEBAR:
      return Object.assign({}, state, { sidebarOpen: action.value });
    case types.TOGGLE_LEGEND:
      return Object.assign({}, state, { legendOpen: action.value });
    case types.ADD_EXTRA_METADATA: {
      for (const colorBy of Object.keys(action.newColorings)) {
        state.coloringsPresentOnTree.add(colorBy);
      }
      let newState = Object.assign({}, state, { coloringsPresentOnTree: state.coloringsPresentOnTree, filters: state.filters });
      if (action.newGeoResolution && !state.panelsAvailable.includes("map")) {
        newState = {
          ...newState,
          geoResolution: action.newGeoResolution.key,
          canTogglePanelLayout: hasMultipleGridPanels([...state.panelsToDisplay, "map"]),
          panelsAvailable: [...state.panelsAvailable, "map"],
          panelsToDisplay: [...state.panelsToDisplay, "map"]
        };
      }
      return newState;
    }
    case types.REMOVE_METADATA: {
      const coloringsPresentOnTree = new Set(state.coloringsPresentOnTree);
      action.nodeAttrsToRemove.forEach((colorBy: string): void => {
        coloringsPresentOnTree.delete(colorBy);
      })
      return {...state, coloringsPresentOnTree};
    }
    case types.UPDATE_VISIBILITY_AND_BRANCH_THICKNESS: {
      const colorScale = Object.assign({}, state.colorScale, { visibleLegendValues: action.visibleLegendValues });
      return Object.assign({}, state, { colorScale: colorScale });
    }
    case types.TOGGLE_TRANSMISSION_LINES:
      return Object.assign({}, state, { showTransmissionLines: action.data });

    case types.LOAD_FREQUENCIES:
      return {...state, normalizeFrequencies: action.normalizeFrequencies};
    case types.FREQUENCY_MATRIX: {
      if (Object.hasOwnProperty.call(action, "normalizeFrequencies")) {
        return Object.assign({}, state, { normalizeFrequencies: action.normalizeFrequencies });
      }
      return state;
    }
    case types.CHANGE_MEASUREMENTS_COLLECTION: // fallthrough
    case types.CHANGE_MEASUREMENTS_COLOR_GROUPING: // fallthrough
    case types.CHANGE_MEASUREMENTS_DISPLAY: // fallthrough
    case types.CHANGE_MEASUREMENTS_GROUP_BY: // fallthrough
    case types.TOGGLE_MEASUREMENTS_OVERALL_MEAN: // fallthrough
    case types.TOGGLE_MEASUREMENTS_THRESHOLD: // fallthrough
    case types.APPLY_MEASUREMENTS_FILTER:
      return {...state, ...action.controls};
    /**
     * Currently the CHANGE_ZOOM action (entropy panel zoom changed) does not
     * update the zoomMin/zoomMax, and as such they only represent the initially
     * requested zoom range. The following commented out code will keep the
     * state in sync, but corresponding changes will be  required to the entropy
     * code.
     */
    // case types.CHANGE_ZOOM: // this is the entropy panel zoom
    //   return {...state, zoomMin: action.zoomc[0], zoomMax: action.zoomc[1]};
    default:
      return state;
  }
};

export default Controls;

function getInitialSidebarState(): {
  sidebarOpen: boolean
  setDefault: boolean
} {
  return {
    sidebarOpen: window.innerWidth > controlsHiddenWidth,
    setDefault: false
  };
}
