import { parse } from 'date-fns'
import { TTraining, TExerciseInstance, TBlockInstance, TBlock, TExercise, TRating } from './types'
import { TrainingQuery, Exercise, Block, ExerciseInstance, BlockInstance } from '../gql'
import { isArray, transform, isEqual, isObject } from 'lodash'

/**
 * Takes a block of exercises and calculates the duration in seconds.
 * @param block
 */
export function calculateDuration(blocks?: TBlockInstance | TBlockInstance[]): number {
  if (!blocks) return 0
  const blocksArray = isArray(blocks) ? blocks : [blocks]
  const duration = blocksArray.map((blockInstance) => {
    const blockRounds = blockInstance.rounds ?? 1
    const blockDuration =
      blockInstance.block?.duration ?? calculateDuration(blockInstance.block?.blocks) ?? 0
    const blockRest = blockInstance.block?.rest ?? 0
    return blockRounds * (blockDuration + blockRest)
  })
  return duration.reduce((a, b) => a + b, 0)
}

/**
 * Formats a time in seconds into 0:00
 * @param seconds
 */
export function formatTime(seconds: number) {
  return `${Math.floor(seconds / 60)}:${(seconds % 60).toString().padStart(2, '0')}`
}

/**
 * Takes an array of exercises and formats them into
 * 4x Exercise 1 - Exercise 2 - 2x Exercise 3
 * @param exercises
 */
export function printExercises(exercises: TExerciseInstance[]) {
  return exercises
    .map((exerciseInstance) =>
      exerciseInstance.repetitions && exerciseInstance.repetitions > 1
        ? `${exerciseInstance.repetitions}x ${exerciseInstance.exercise?.name}`
        : exerciseInstance.exercise?.name
    )
    .join(' - ')
}

/**
 * Takes an array of rating and calculates the average rating
 * @param ratings
 */
export function calculateRating(ratings: TRating[]) {
  const numberOfRatings = ratings.length
  const sumOfRatings = ratings.reduce((accumulator, rating) => accumulator + rating.value, 0)
  return numberOfRatings ? sumOfRatings / numberOfRatings : '-'
}

export function blockArrayToDB() {}

const Weekdays = {
  Monday: 1,
  Tuesday: 2,
  Wednesday: 3,
  Thursday: 4,
  Friday: 5,
  Saturday: 6,
  Sunday: 0,
}
type TWeekdays = keyof typeof Weekdays
/**
 * Get the date of the next weekday.
 * @param weekDay Sunday=0, Monday=1, Tuesday=2
 * @param time Format: HH:mm
 */
export function nextTime(weekDay: TWeekdays, time: string) {
  const nextTime = parse(time, 'HH:mm', new Date())
  nextTime.setDate(nextTime.getDate() + ((Weekdays[weekDay] + 7 - nextTime.getDay()) % 7))
  return nextTime.toISOString()
}

function randomID() {
  return `++${Math.random()
    .toString(36)
    .replace(/[^a-zA-Z0-9]+/g, '')
    .substr(0, 10)}`
}

export function emptyExercise(input?: Partial<Exercise>) {
  const emptyExercise: TExercise = {
    id: randomID(),
    name: '',
    description: '',
    videos: [],
    pictures: [],
    targets: [],
    baseExercise: [],
  }
  return { ...emptyExercise, ...input }
}

export function emptyExerciseInstance(input?: Partial<ExerciseInstance>) {
  const emptyExerciseInstance: TExerciseInstance = {
    id: randomID(),
    order: 0,
    exercise: emptyExercise(),
  }
  return { ...emptyExerciseInstance, ...input }
}

export function emptyBlock(input?: Partial<Block>) {
  const emptyBlock: TBlock = {
    id: randomID(),
    title: '',
    format: { id: '', name: '', description: '' },
    videos: [],
    pictures: [],
    blocks: [],
    exercises: [],
  }
  return { ...emptyBlock, ...input }
}

export function emptyBlockInstance(input?: Partial<BlockInstance>) {
  const emptyBlockInstance: TBlockInstance = {
    id: randomID(),
    order: 0,
  }
  return { ...emptyBlockInstance, ...input }
}

export function emptyTraining(input?: TTraining) {
  const emptyTraining: TTraining = {
    id: randomID(),
    title: '',
    type: { id: '', name: '', description: '' },
    createdAt: '',
    trainingDate: nextTime('Tuesday', '11:45'),
    published: false,
    blocks: [],
  }
  return { ...emptyTraining, ...input }
}

export function collectMutations(arr: any[]) {
  const create: any[] = []
  const connect: any[] = []
  const del: any[] = []
  const update: any[] = []
  arr.forEach((val) => {
    if (val?.connect) connect.push(val.connect)
    if (val?.create) create.push(val.create)
    if (val?.delete) del.push(val.delete)
    if (val?.update) update.push(val.update)
  })
  const returnObject: {
    connect?: any
    delete?: any
    create?: any
    update?: any
  } = {}
  if (connect.length > 0) returnObject.connect = connect
  if (del.length > 0) returnObject.delete = del
  if (update.length > 0) returnObject.update = update
  if (create.length > 0) returnObject.create = create

  if (Object.keys(returnObject).length > 0) return returnObject
  else if (arr.length > 0) return { set: arr }
  else return undefined
}

function isNotInDB(key: any, val: any) {
  return key === '__typename' || (key === 'id' && typeof val === 'string' && val.startsWith('++'))
}

export function transformArrayToDB(acc: any, val: any, key: any, object: any) {
  if (isNotInDB(key, val)) return
  if (isArray(val)) {
    const collectedArray = collectMutations(transform(val, transformArrayToDB))
    if (collectedArray) {
      acc[key] = collectedArray
    }
  } else if (typeof val === 'object' && !!val) {
    if (val?.id && val.id.startsWith('++')) {
      acc[key] = { create: transform(val, transformArrayToDB) }
    } else if (val?.id && val.id.startsWith('--')) {
      acc[key] = { delete: { id: val.id.substr(2) } }
    } else {
      const { id, ...data } = val
      if (id?.startsWith('@@')) {
        if (typeof key === 'string') {
          acc[key] = { update: transform(data, transformArrayToDB) }
        } else {
          acc[key] = {
            update: {
              where: { id: id.substr(2) },
              data: transform(data, transformArrayToDB),
            },
          }
        }
      } else {
        acc[key] = { connect: { id } }
      }
    }
  } else {
    // copy the value
    acc[key] = val
  }
}

function isScalarArray(object: any) {
  return (
    isArray(object) &&
    object.every(
      (item) =>
        typeof item === 'string' ||
        typeof item === 'number' ||
        typeof item === 'boolean' ||
        typeof item === 'bigint'
    )
  )
}

interface IDbItem {
  id?: string
}

export function diffDB(newObject: any = {}, oldObject: any = {}) {
  // IDs starting with -- are for deletion.
  if (newObject.id && newObject.id.startsWith('--')) {
    return { id: newObject['id'] }
  }

  // leave arrays of numbers and strings alone
  if (isScalarArray(newObject)) {
    if (!isEqual(newObject, oldObject)) return newObject
    else return undefined
  }

  // transform everything else
  const transformResult = transform(newObject, (result: any, value: any, key: string) => {
    if (!isEqual(value, oldObject[key])) {
      const newValue =
        isObject(value) && isObject(oldObject[key]) ? diffDB(value, oldObject[key]) : value
      if (newValue !== undefined) {
        result[key] = newValue
      }
    }
  })

  // detects updates, return object if there was a change.
  if (Object.keys(transformResult).length > 0) {
    if (!transformResult.id && oldObject.id) {
      transformResult.id = `@@${oldObject.id}`
    }
    return transformResult
  } else {
    return undefined
  }
}

export function prepareDataForDB<T>(formData: T, dbData: T) {
  const changes = diffDB(formData, dbData)
  const gqlData = transform(changes, transformArrayToDB)
  return gqlData
}

export function countBlocksAndExercises(block?: TBlock): [number, number] {
  const [blocks, exercises] = block?.blocks?.reduce(
    (accumulator, blockInstance) => {
      const [accumulatedBlocks, accumulatedExercises] = accumulator
      const [subBlocks, subExercises] = countBlocksAndExercises(blockInstance.block)
      const currentBlocks = blockInstance.block?.blocks?.length ?? 0
      const currentExercises = blockInstance.block?.exercises?.length ?? 0
      return [
        accumulatedBlocks + currentBlocks + subBlocks,
        accumulatedExercises + currentExercises + subExercises,
      ]
    },
    [0, 0]
  ) ?? [0, 0]
  return [blocks + (block?.blocks?.length ?? 0), exercises + (block?.exercises?.length ?? 0)]
}
