/*
 This file is part of GNU Taler
 (C) 2022 Taler Systems S.A.
 GNU Taler is free software; you can redistribute it and/or modify it under the
 terms of the GNU General Public License as published by the Free Software
 Foundation; either version 3, or (at your option) any later version.
 GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
 You should have received a copy of the GNU General Public License along with
 GNU Taler; see the file COPYING.  If not, see 
 */
import { AbsoluteTime, AmountJson, Amounts, DenominationInfo, TalerProtocolTimestamp } from "@gnu-taler/taler-util";
import { useState } from "preact/hooks";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import * as wxApi from "../../wxApi.js";
import { FeeDescription, FeeDescriptionPair, OperationMap, Props, State } from "./index.js";
export function useComponentState(
  { onCancel, onSelection, currency }: Props,
  api: typeof wxApi,
): State {
  const hook = useAsyncAsHook(api.listExchanges);
  const initialValue = 0
  const [value, setValue] = useState(String(initialValue));
  if (!hook) {
    return {
      status: "loading",
      error: undefined,
    }
  }
  if (hook.hasError) {
    return {
      status: "loading-uri",
      error: hook,
    };
  }
  const exchanges = hook.response.exchanges;
  if (exchanges.length === 0) {
    return {
      status: "no-exchanges",
      error: undefined
    }
  }
  const original = exchanges[initialValue];
  const selected = exchanges[Number(value)];
  let nextFeeUpdate = TalerProtocolTimestamp.never();
  nextFeeUpdate = Object.values(selected.wireInfo.feesForType).reduce(
    (prev, cur) => {
      return cur.reduce((p, c) => nearestTimestamp(p, c.endStamp), prev);
    },
    nextFeeUpdate,
  );
  nextFeeUpdate = selected.denominations.reduce((prev, cur) => {
    return [
      cur.stampExpireWithdraw,
      cur.stampExpireLegal,
      cur.stampExpireDeposit,
    ].reduce(nearestTimestamp, prev);
  }, nextFeeUpdate);
  const timeline: OperationMap = {
    deposit: createDenominationTimeline(
      selected.denominations,
      "stampExpireDeposit",
      "feeDeposit",
    ),
    refresh: createDenominationTimeline(
      selected.denominations,
      "stampExpireWithdraw",
      "feeRefresh",
    ),
    refund: createDenominationTimeline(
      selected.denominations,
      "stampExpireWithdraw",
      "feeRefund",
    ),
    withdraw: createDenominationTimeline(
      selected.denominations,
      "stampExpireWithdraw",
      "feeWithdraw",
    ),
  };
  const exchangeMap = exchanges.reduce((prev, cur, idx) => ({ ...prev, [cur.exchangeBaseUrl]: String(idx) }), {} as Record)
  if (original === selected) {
    return {
      status: "ready",
      exchanges: {
        list: exchangeMap,
        value: value,
        onChange: async (v) => {
          setValue(v)
        }
      },
      error: undefined,
      nextFeeUpdate: AbsoluteTime.fromTimestamp(nextFeeUpdate),
      onClose: {
        onClick: onCancel
      },
      selected,
      timeline
    }
  }
  const originalTimeline: OperationMap = {
    deposit: createDenominationTimeline(
      original.denominations,
      "stampExpireDeposit",
      "feeDeposit",
    ),
    refresh: createDenominationTimeline(
      original.denominations,
      "stampExpireWithdraw",
      "feeRefresh",
    ),
    refund: createDenominationTimeline(
      original.denominations,
      "stampExpireWithdraw",
      "feeRefund",
    ),
    withdraw: createDenominationTimeline(
      original.denominations,
      "stampExpireWithdraw",
      "feeWithdraw",
    ),
  };
  const pairTimeline: OperationMap = {
    deposit: createDenominationPairTimeline(timeline.deposit, originalTimeline.deposit),
    refresh: createDenominationPairTimeline(timeline.refresh, originalTimeline.refresh),
    refund: createDenominationPairTimeline(timeline.refund, originalTimeline.refund),
    withdraw: createDenominationPairTimeline(timeline.withdraw, originalTimeline.withdraw),
  }
  return {
    status: "comparing",
    exchanges: {
      list: exchangeMap,
      value: value,
      onChange: async (v) => {
        setValue(v)
      }
    },
    error: undefined,
    nextFeeUpdate: AbsoluteTime.fromTimestamp(nextFeeUpdate),
    onReset: {
      onClick: async () => {
        setValue(String(initialValue))
      }
    },
    onSelect: {
      onClick: async () => {
        onSelection(selected.exchangeBaseUrl)
      }
    },
    selected,
    pairTimeline,
  }
}
function nearestTimestamp(
  first: TalerProtocolTimestamp,
  second: TalerProtocolTimestamp,
): TalerProtocolTimestamp {
  const f = AbsoluteTime.fromTimestamp(first);
  const s = AbsoluteTime.fromTimestamp(second);
  const a = AbsoluteTime.min(f, s);
  return AbsoluteTime.toTimestamp(a);
}
export interface TimePoint {
  type: "start" | "end";
  moment: AbsoluteTime;
  denom: DenominationInfo;
}
/**
 * Given a list of denominations with the same value and same period of time:
 * return the one that will be used.
 * The best denomination is the one that will minimize the fee cost.
 *
 * @param list denominations of same value
 * @returns
 */
function selectBestForOverlappingDenominations(
  list: DenominationInfo[],
): DenominationInfo | undefined {
  let minDeposit: DenominationInfo | undefined = undefined;
  list.forEach((e) => {
    if (minDeposit === undefined) {
      minDeposit = e;
      return;
    }
    if (Amounts.cmp(minDeposit.feeDeposit, e.feeDeposit) > -1) {
      minDeposit = e;
    }
  });
  return minDeposit;
}
type PropsWithReturnType = Exclude<
  {
    [K in keyof T]: T[K] extends F ? K : never;
  }[keyof T],
  undefined
>;
/**
 * Takes two list and create one with one timeline.
 * For any element in the position "p" on the left or right "list", then
 * list[p].until should be equal to list[p+1].from
 * 
 * @see {createDenominationTimeline}
 * 
 * @param left list denominations @type {FeeDescription}
 * @param right list denominations @type {FeeDescription}
 * @returns list of pairs for the same time
 */
export function createDenominationPairTimeline(left: FeeDescription[], right: FeeDescription[]): FeeDescriptionPair[] {
  //both list empty, discarded
  if (left.length === 0 && right.length === 0) return [];
  const pairList: FeeDescriptionPair[] = [];
  let li = 0;
  let ri = 0;
  while (li < left.length && ri < right.length) {
    const currentValue = Amounts.cmp(left[li].value, right[ri].value) < 0 ? left[li].value : right[ri].value;
    let ll = 0 //left length (until next value)
    while (li + ll < left.length && Amounts.cmp(left[li + ll].value, currentValue) === 0) {
      ll++
    }
    let rl = 0 //right length (until next value)
    while (ri + rl < right.length && Amounts.cmp(right[ri + rl].value, currentValue) === 0) {
      rl++
    }
    const leftIsEmpty = ll === 0
    const rightIsEmpty = rl === 0
    //check which start after, add gap so both list starts at the same time
    // one list may be empty
    const leftStarts: AbsoluteTime =
      leftIsEmpty ? { t_ms: "never" } : left[li].from;
    const rightStarts: AbsoluteTime =
      rightIsEmpty ? { t_ms: "never" } : right[ri].from;
    //first time cut is the smallest time
    let timeCut: AbsoluteTime = leftStarts;
    if (AbsoluteTime.cmp(leftStarts, rightStarts) < 0) {
      const ends =
        rightIsEmpty ? left[li + ll - 1].until : right[0].from;
      right.splice(ri, 0, {
        from: leftStarts,
        until: ends,
        value: left[li].value,
      });
      rl++;
      timeCut = leftStarts
    }
    if (AbsoluteTime.cmp(leftStarts, rightStarts) > 0) {
      const ends =
        leftIsEmpty ? right[ri + rl - 1].until : left[0].from;
      left.splice(li, 0, {
        from: rightStarts,
        until: ends,
        value: right[ri].value,
      });
      ll++;
      timeCut = rightStarts
    }
    //check which ends sooner, add gap so both list ends at the same time
    // here both list are non empty
    const leftEnds: AbsoluteTime = left[li + ll - 1].until;
    const rightEnds: AbsoluteTime = right[ri + rl - 1].until;
    if (AbsoluteTime.cmp(leftEnds, rightEnds) > 0) {
      right.splice(ri + rl, 0, {
        from: rightEnds,
        until: leftEnds,
        value: left[0].value,
      });
      rl++;
    }
    if (AbsoluteTime.cmp(leftEnds, rightEnds) < 0) {
      left.splice(li + ll, 0, {
        from: leftEnds,
        until: rightEnds,
        value: right[0].value,
      });
      ll++;
    }
    //now both lists are non empty and (starts,ends) at the same time
    while (li < left.length && ri < right.length && Amounts.cmp(left[li].value, right[ri].value) === 0) {
      if (AbsoluteTime.cmp(left[li].from, timeCut) !== 0 && AbsoluteTime.cmp(right[ri].from, timeCut) !== 0) {
        // timeCut comes from the latest "until" (expiration from the previous)
        // and this value comes from the latest left or right
        // it should be the same as the "from" from one of the latest left or right
        // otherwise it means that there is missing a gap object in the middle
        // the list is not complete and the behavior is undefined
        throw Error('one of the list is not completed: list[i].until !== list[i+1].from')
      }
      pairList.push({
        left: left[li].fee,
        right: right[ri].fee,
        from: timeCut,
        until: AbsoluteTime.never(),
        value: currentValue,
      });
      if (left[li].until.t_ms === right[ri].until.t_ms) {
        timeCut = left[li].until;
        ri++;
        li++;
      } else if (left[li].until.t_ms < right[ri].until.t_ms) {
        timeCut = left[li].until;
        li++;
      } else if (left[li].until.t_ms > right[ri].until.t_ms) {
        timeCut = right[ri].until;
        ri++;
      }
      pairList[pairList.length - 1].until = timeCut
      if (li < left.length && Amounts.cmp(left[li].value, pairList[pairList.length - 1].value) !== 0) {
        //value changed, should break
        //this if will catch when both (left and right) change at the same time
        //if just one side changed it will catch in the while condition
        break;
      }
    }
  }
  //one of the list left or right can still have elements
  if (li < left.length) {
    let timeCut = pairList.length > 0 && Amounts.cmp(pairList[pairList.length - 1].value, left[li].value) === 0 ? pairList[pairList.length - 1].until : left[li].from;
    while (li < left.length) {
      pairList.push({
        left: left[li].fee,
        right: undefined,
        from: timeCut,
        until: left[li].until,
        value: left[li].value,
      })
      timeCut = left[li].until
      li++;
    }
  }
  if (ri < right.length) {
    let timeCut = pairList.length > 0 && Amounts.cmp(pairList[pairList.length - 1].value, right[ri].value) === 0 ? pairList[pairList.length - 1].until : right[ri].from;
    while (ri < right.length) {
      pairList.push({
        right: right[ri].fee,
        left: undefined,
        from: timeCut,
        until: right[ri].until,
        value: right[ri].value,
      })
      timeCut = right[ri].until
      ri++;
    }
  }
  return pairList
}
/**
 * Create a usage timeline with the denominations given.
 *
 * If there are multiple denominations that can be used, the list will
 * contain the one that minimize the fee cost. @see selectBestForOverlappingDenominations
 *
 * @param list list of denominations
 * @param periodProp property of element of the list that will be used as end of the usage period
 * @param feeProp property of the element of the list that will be used as fee reference
 * @returns  list of @type {FeeDescription} sorted by usage period
 */
export function createDenominationTimeline(
  list: DenominationInfo[],
  periodProp: PropsWithReturnType,
  feeProp: PropsWithReturnType,
): FeeDescription[] {
  const points = list
    .reduce((ps, denom) => {
      //exclude denoms with bad configuration
      if (denom.stampStart.t_s >= denom[periodProp].t_s) {
        throw Error(`denom ${denom.denomPubHash} has start after the end`);
        // return ps;
      }
      ps.push({
        type: "start",
        moment: AbsoluteTime.fromTimestamp(denom.stampStart),
        denom,
      });
      ps.push({
        type: "end",
        moment: AbsoluteTime.fromTimestamp(denom[periodProp]),
        denom,
      });
      return ps;
    }, [] as TimePoint[])
    .sort((a, b) => {
      const v = Amounts.cmp(a.denom.value, b.denom.value);
      if (v != 0) return v;
      const t = AbsoluteTime.cmp(a.moment, b.moment);
      if (t != 0) return t;
      if (a.type === b.type) return 0;
      return a.type === "start" ? 1 : -1;
    });
  const activeAtTheSameTime: DenominationInfo[] = [];
  return points.reduce((result, cursor, idx) => {
    const hash = cursor.denom.denomPubHash;
    if (!hash)
      throw Error(
        `denomination without hash ${JSON.stringify(
          cursor.denom,
          undefined,
          2,
        )}`,
      );
    let prev = result.length > 0 ? result[result.length - 1] : undefined;
    const prevHasSameValue =
      prev && Amounts.cmp(prev.value, cursor.denom.value) === 0;
    if (prev) {
      if (prevHasSameValue) {
        prev.until = cursor.moment;
        if (prev.from.t_ms === prev.until.t_ms) {
          result.pop();
          prev = result[result.length - 1];
        }
      } else {
        // the last end adds a gap that we have to remove
        result.pop();
      }
    }
    //update the activeAtTheSameTime list
    if (cursor.type === "end") {
      const loc = activeAtTheSameTime.findIndex((v) => v.denomPubHash === hash);
      if (loc === -1) {
        throw Error(`denomination ${hash} has an end but no start`);
      }
      activeAtTheSameTime.splice(loc, 1);
    } else if (cursor.type === "start") {
      activeAtTheSameTime.push(cursor.denom);
    } else {
      const exhaustiveCheck: never = cursor.type;
      throw new Error(`not TimePoint defined for type: ${exhaustiveCheck}`);
    }
    if (idx == points.length - 1) {
      //this is the last element in the list, prevent adding
      //a gap in the end
      if (cursor.type !== "end") {
        throw Error(
          `denomination ${hash} starts after ending or doesn't have an ending`,
        );
      }
      if (activeAtTheSameTime.length > 0) {
        throw Error(
          `there are ${activeAtTheSameTime.length} denominations without ending`,
        );
      }
      return result;
    }
    const current = selectBestForOverlappingDenominations(activeAtTheSameTime);
    if (current) {
      if (
        prev === undefined || //is the first
        !prev.fee || //is a gap
        Amounts.cmp(prev.fee, current[feeProp]) !== 0 // prev has the same fee
      ) {
        result.push({
          value: cursor.denom.value,
          from: cursor.moment,
          until: AbsoluteTime.never(), //not yet known
          fee: current[feeProp],
        });
      } else {
        prev.until = cursor.moment;
      }
    } else {
      result.push({
        value: cursor.denom.value,
        from: cursor.moment,
        until: AbsoluteTime.never(), //not yet known
      });
    }
    return result;
  }, [] as FeeDescription[]);
}