import React, { createContext } from "react"
import flatted, { parse, stringify } from "flatted"

import { sanitise } from "~/functions/sanitise"
import { smarts } from "~/smarts"

export interface ThingtimeContextInterface {
  thingtime: any
  setThingtime: any
  getThingtime: any
  thingtimeRef: any
  loading: boolean
}

export const ThingtimeContext = createContext<ThingtimeContextInterface | null>(
  null
)

try {
  window.smarts = smarts
  window.flatted = {
    parse,
    stringify,
  }
} catch (err) {
  // nothing
}

const force = {
  settings: {
    undoLimit: 999,
    // commander: {
    //   nav: {
    //     commanderActive: false,
    //     clearCommanderOnToggle: true,
    //     clearCommanderContextOnToggle: true,
    //     hideSuggestionsOnToggle: true,
    // },
    // },
  },
  //   Content: {
  //     hidden1: "Edit this to your heart's desire.",
  //     "How?": "Just search for Content and edit the value to whatever you want.",
  //     "Example:": `Content = New Content!
  // Content.Nested Content = New Nested Content!
  //     `,
  //   },
  version: 24,
}

const newVersionData = {
  Content: {
    hidden1: "Edit this to your heart's desire.",
    "How?": "Just search for Content and edit the value to whatever you want.",
    "Example:": `Content = New Content!
      Content.Nested Content = New Nested Content!
    `,
  },
}

const initialValues = {
  settings: {
    commander: {
      nav: {
        commanderActive: false,
        clearCommanderOnToggle: true,
        clearCommanderContextOnToggle: true,
        hideSuggestionsOnToggle: true,
      },
    },
  },
  Content: {
    hidden1: "Edit this to your heart's desire.",
    "How?": "Just search for Content and edit the value to whatever you want.",
    "Example:": `
Content = New Content!
Content.Nested Content = New Nested Content!
    `,
  },
}

// const initialThingtime = createInitialThingtime()
const initialThingtime = smarts.merge(initialValues, force)

// initialise thingtime
initialThingtime.thingtime = initialThingtime
initialThingtime.tt = initialThingtime

export const ThingtimeProvider = (props: any): JSX.Element => {
  const [thingtimeReference, rawSet] = React.useState()

  const thingtimeRef = React.useRef(thingtimeReference)
  const stateRef = React.useRef({
    c: 1,
  })

  const [loading, setLoading] = React.useState(true)

  const set = React.useCallback((newThingtime, ignoreUndoRedo?: any) => {
    const newThingtimeReference = {
      ...newThingtime,
    }

    newThingtimeReference.tt = newThingtimeReference
    newThingtimeReference.thingtime = newThingtimeReference

    // store undo/redo history
    if (!ignoreUndoRedo) {
      try {
        console.log(
          "ThingtimeProvider setting thingtime to localStorage",
          newThingtimeReference
        )
        // setTimeout(() => {
        const stringified = stringify(newThingtimeReference)
        let undoHistory = []
        try {
          const undoHistoryString = window.localStorage.getItem("undoHistory")
          const parsedUndoHistory = JSON.parse(undoHistoryString)
          if (parsedUndoHistory instanceof Array) {
            undoHistory = parsedUndoHistory
          }
        } catch {
          // nothing
        }
        // if last undoHistory does not equal new undo history
        if (undoHistory[undoHistory.length - 1]?.value !== stringified) {
          try {
            const limit = newThingtimeReference?.settings?.undoLimit || 999

            if (undoHistory?.length > limit) {
              undoHistory = undoHistory.slice(undoHistory.length - limit)
            }

            undoHistory.push({
              timestamp: Date.now(),
              value: stringify(newThingtimeReference),
            })
            const undoHistoryNewString = JSON.stringify(undoHistory)
            window.localStorage.setItem("undoHistory", undoHistoryNewString)
          } catch {
            // nothing
          }
        }
      } catch (err) {
        console.error("There was an error saving thingtime to localStorage")
      }
      const saveRedo = false
      if (saveRedo) {
        try {
          console.log(
            "ThingtimeProvider setting thingtime to localStorage",
            newThingtimeReference
          )
          // setTimeout(() => {
          const stringified = stringify(newThingtimeReference)
          let redoHistory = []
          try {
            const redoHistoryString = window.localStorage.getItem("redoHistory")
            const parsedRedoHistory = JSON.parse(redoHistoryString)
            if (parsedRedoHistory instanceof Array) {
              redoHistory = parsedRedoHistory
            }
          } catch {
            // nothing
          }
          if (redoHistory[redoHistory.length - 1]?.value !== stringified) {
            try {
              const limit = newThingtimeReference?.settings?.redoLimit || 999

              if (redoHistory?.length > limit) {
                redoHistory = redoHistory.slice(redoHistory.length - limit)
              }

              redoHistory.push({
                timestamp: Date.now(),
                value: stringify(newThingtimeReference),
              })
              const redoHistoryNewString = JSON.stringify(redoHistory)
              window.localStorage.setItem("redoHistory", redoHistoryNewString)
            } catch {
              // nothing
            }
          }
        } catch (err) {
          console.error("There was an error saving thingtime to localStorage")
        }
      }
    }

    rawSet(newThingtimeReference)
  }, [])

  const setThingtime = React.useCallback(
    (path, value) => {
      // TODO: make this a lot safer
      if (["thingtime", "tt"]?.includes(path)) {
        if (value) {
          set(value)
          return
        }
      }

      const newThingtime = thingtimeReference

      const paths = smarts.parsePropertyPath(path)

      // find first parent where a path is undefined
      // paths is array of path parts such as ["path1", "path2", "path3"]
      // we want to create a new reference at the first object which has an undefined part of the path
      // and is an object itself
      // so that react will detect the change and re-render
      // "path1" = { ...thingtime["path1"] } if path1.path2 undefined
      // "path1.path2" = { ...thingtime["path1"]["path2"] } if path1.path2.path3 undefined
      // "path1.path2.path3" = { ...thingtime["path1"]["path2"]["path3"] }
      // etc
      let done = false
      paths.forEach((pathPart, index) => {
        if (!done) {
          const pathParts = paths.slice(0, index + 1)
          const tmpPath = pathParts.join(".")
          const parentPath = pathParts.slice(0, -1).join(".")

          const valAtPath = smarts.getsmart(newThingtime, tmpPath)

          if (parentPath) {
            if (typeof valAtPath !== "object" || valAtPath === null) {
              const parentVal = smarts.getsmart(newThingtime, parentPath)
              if (typeof parentVal === "object") {
                const newParent = Array.isArray(parentVal)
                  ? [...parentVal]
                  : { ...parentVal }
                smarts.setsmart(newThingtime, parentPath, newParent)
              }
              done = true
            }
          }
        }
      })

      // TODO: make thingtime settable
      newThingtime.thingtime = newThingtime
      newThingtime.tt = newThingtime

      console.log(
        "ThingtimeProvider setting newThingtime value at path",
        '"' + path + '"',
        "value: ",
        value
      )

      smarts.setsmart(newThingtime, path, value)

      set(newThingtime)
    },
    [thingtimeReference, set]
  )

  const getThingtime = React.useCallback(
    (...args) => {
      const rawPath = args[0]
      const path = rawPath

      if (!path) {
        return thingtimeReference
      }

      // do we need to sanitise?
      // const path = sanitise(rawPath)
      console.log("ThingtimeProvider getting thingtime at path", path)
      // console.trace("Getting thingtime at path", path)
      return smarts.getsmart(thingtimeReference, path)
    },
    [thingtimeReference]
  )

  const populatePaths = React.useCallback((obj, path, paths, seen = []) => {
    try {
      Object.keys(obj).forEach((key) => {
        const val = obj[key]
        const newPath = path ? `${path}${path ? "." : ""}${key}` : key
        if (typeof val === "object") {
          paths.push(newPath)
          if (!seen?.includes(val)) {
            seen.push(val)
            populatePaths(val, newPath, paths, seen)
          }
        } else {
          paths.push(newPath)
        }
      })
    } catch {
      // nothing
    }
  }, [])

  const paths = React.useMemo(() => {
    // const paths = ["tt", "thingtime", "."]
    const paths = []

    // populatePaths(thingtime, commandPath)
    populatePaths(thingtimeReference, "", paths)

    return paths
  }, [populatePaths, thingtimeReference])

  // get thingtime from localstorage
  React.useEffect(() => {
    try {
      const thingtimeFromLocalStorage = window.localStorage.getItem("thingtime")

      if (thingtimeFromLocalStorage) {
        const parsed = parse(thingtimeFromLocalStorage)
        console.log(
          "ThingtimeProvider thingtime restored from localstorage",
          parsed
        )
        if (parsed) {
          const localIsValid =
            !parsed.version || parsed.version >= force.version
          let newThingtime = smarts.merge(force, initialThingtime)
          if (localIsValid) {
            newThingtime = smarts.merge(parsed, newThingtime)
          } else {
            const withVersionUpdates = smarts.merge(newVersionData, parsed)
            newThingtime = smarts.merge(force, withVersionUpdates)
          }
          console.log(
            "ThingtimeProvider restoring thingtime from localStorage",
            newThingtime
          )
          set(newThingtime)
        }
      } else {
        set(initialThingtime)
      }
    } catch (err) {
      console.error("There was an error getting thingtime from localStorage")
    }
    setLoading(false)
  }, [])

  // thingtime change listener
  React.useEffect(() => {
    try {
      window.setThingtime = setThingtime
      window.getThingtime = getThingtime
      window.thingtime = thingtimeReference
      window.tt = thingtimeReference
    } catch {
      // nothing
    }

    if (stateRef.current.initialized) {
      try {
        console.log(
          "ThingtimeProvider setting thingtime to localStorage",
          thingtimeReference
        )
        // setTimeout(() => {
        const stringified = stringify(thingtimeReference)
        window.localStorage.setItem("thingtime", stringified)
        // }, 600)
      } catch (err) {
        console.error("There was an error saving thingtime to localStorage")
      }
    } else {
      stateRef.current.initialized = true
    }

    thingtimeRef.current = thingtimeReference

    const keyListener = (e) => {
      // if ctrl + z, restore thingtime from localstorage history

      console.log("ThingtimeProvider listened to key event e?.key", e?.key)
      console.log(
        "ThingtimeProvider listened to key event e?.ctrlKey",
        e?.ctrlKey
      )
      console.log(
        "ThingtimeProvider listened to key event e?.shiftKey",
        e?.shiftKey
      )
      console.log(
        "ThingtimeProvider listened to key event e?.metaKey",
        e?.metaKey
      )

      if ((e?.ctrlKey || e?.metaKey) && e?.key === "z") {
        e?.preventDefault()

        console.log("ThingtimeProvider detected undo/redo request")

        if (e.shiftKey) {
          // redo
          console.log("ThingtimeProvider redo")
          const redoHistoryString = window.localStorage.getItem("redoHistory")
          const parsedRedoHistory = JSON.parse(redoHistoryString)
          if (parsedRedoHistory instanceof Array) {
            const last = parsedRedoHistory[parsedRedoHistory.length - 1]
            if (last) {
              const parsed = parse(last.value)
              if (parsed) {
                // remove restored state from history
                // const currentHistory = parsedRedoHistory.pop()
                parsedRedoHistory.pop()
                // parsedRedoHistory.push(currentHistory)
                const newRedoHistoryString = JSON.stringify(parsedRedoHistory)
                window.localStorage.setItem("redoHistory", newRedoHistoryString)

                // save old/current state to undo history
                let undoHistory = []
                try {
                  const undoHistoryString =
                    window.localStorage.getItem("undoHistory")
                  const parsedUndoHistory = JSON.parse(undoHistoryString)
                  if (parsedUndoHistory instanceof Array) {
                    undoHistory = parsedUndoHistory
                  }
                } catch {
                  // nothing
                }
                try {
                  undoHistory.push({
                    timestamp: Date.now(),
                    value: stringify(thingtimeReference),
                  })
                  const undoHistoryNewString = JSON.stringify(undoHistory)
                  window.localStorage.setItem(
                    "undoHistory",
                    undoHistoryNewString
                  )
                } catch {
                  // nothing
                }

                const newThingtime = parsed
                set(newThingtime, true)
              }
            }
          }
        } else {
          // undo
          console.log("ThingtimeProvider undo")
          try {
            const undoHistoryString = window.localStorage.getItem("undoHistory")
            const parsedUndoHistory = JSON.parse(undoHistoryString)
            if (parsedUndoHistory instanceof Array) {
              const last = parsedUndoHistory[parsedUndoHistory.length - 2]
              if (last) {
                const parsed = parse(last.value)
                if (parsed) {
                  // remove restored state from history

                  const currentHistory = parsedUndoHistory.pop()
                  parsedUndoHistory.pop()
                  parsedUndoHistory.push(currentHistory)

                  const newUndoHistoryString = JSON.stringify(parsedUndoHistory)
                  window.localStorage.setItem(
                    "undoHistory",
                    newUndoHistoryString
                  )

                  // save old/current state to redo history
                  let redoHistory = []
                  try {
                    const redoHistoryString =
                      window.localStorage.getItem("redoHistory")
                    const parsedRedoHistory = JSON.parse(redoHistoryString)
                    if (parsedRedoHistory instanceof Array) {
                      redoHistory = parsedRedoHistory
                    }
                  } catch {
                    // nothing
                  }
                  try {
                    const newValue = stringify(thingtimeReference)
                    // if last history is not the same as new history
                    redoHistory.push({
                      timestamp: Date.now(),
                      value: newValue,
                    })
                    const redoHistoryNewString = JSON.stringify(redoHistory)
                    window.localStorage.setItem(
                      "redoHistory",
                      redoHistoryNewString
                    )
                  } catch {
                    // nothing
                  }

                  const newThingtime = parsed
                  set(newThingtime, true)
                }
              }
            }
          } catch {
            // nothing
          }
        }
      }
    }

    window.addEventListener("keydown", keyListener)

    return () => {
      window.removeEventListener("keydown", keyListener)
    }
  }, [setThingtime, getThingtime, thingtimeReference, set])

  const value = {
    thingtime: thingtimeReference,
    setThingtime,
    getThingtime,
    thingtimeRef,
    paths,
    loading,
  }

  return (
    <ThingtimeContext.Provider value={value}>
      {props?.children}
    </ThingtimeContext.Provider>
  )
}
