import { NullableId, Params, ServiceMethods } from '@feathersjs/feathers'
import appRootPath from 'app-root-path'
import fs from 'fs'
import path from 'path'

import { isDev } from '@xrengine/common/src/config'
import { SceneData, SceneJson } from '@xrengine/common/src/interfaces/SceneInterface'
import defaultSceneSeed from '@xrengine/projects/default-project/default.scene.json'

import { Application } from '../../../declarations'
import { getCacheDomain } from '../../media/storageprovider/getCacheDomain'
import { getCachedURL } from '../../media/storageprovider/getCachedURL'
import { getStorageProvider } from '../../media/storageprovider/storageprovider'
import logger from '../../ServerLogger'
import { cleanString } from '../../util/cleanString'
import { cleanSceneDataCacheURLs, parseSceneDataCacheURLs } from './scene-parser'

const NEW_SCENE_NAME = 'New-Scene'

const sceneAssetFiles = ['.scene.json', '.thumbnail.jpeg', '.envmap.png']

export const getSceneData = async (
  projectName: string,
  sceneName: string,
  metadataOnly: boolean,
  internal = false,
  storageProviderName?: string
) => {
  const storageProvider = getStorageProvider(storageProviderName)
  const scenePath = `projects/${projectName}/${sceneName}.scene.json`
  const thumbnailPath = `projects/${projectName}/${sceneName}.thumbnail.jpeg`

  const cacheDomain = getCacheDomain(storageProvider, internal)
  const thumbnailUrl = getCachedURL(thumbnailPath, cacheDomain)

  const sceneExists = await storageProvider.doesExist(`${sceneName}.scene.json`, `projects/${projectName}/`)
  if (sceneExists) {
    const sceneResult = await storageProvider.getCachedObject(scenePath)
    const sceneData: SceneData = {
      name: sceneName,
      project: projectName,
      thumbnailUrl: thumbnailUrl,
      scene: metadataOnly ? undefined! : parseSceneDataCacheURLs(JSON.parse(sceneResult.Body.toString()), cacheDomain)
    }
    return sceneData
  }
  throw new Error(`No scene named ${sceneName} exists in project ${projectName}`)
}

interface UpdateParams {
  sceneName: string
  sceneData?: SceneJson
  thumbnailBuffer?: ArrayBuffer | Buffer // ArrayBuffer on client, Buffer on server
  storageProviderName?: string
}

interface RenameParams {
  newSceneName: string
  oldSceneName: string
  projectName: string
  storageProviderName?: string
}

export class Scene implements ServiceMethods<any> {
  app: Application
  docs: any

  constructor(app: Application) {
    this.app = app
  }

  async setup() {}

  async find(params?: Params): Promise<{ data: SceneData[] }> {
    const projects = await this.app.service('project').find(params)

    const scenes: SceneData[] = []
    for (const project of projects.data) {
      const { data } = await this.app
        .service('scene-data')
        .get({ projectName: project.name, metadataOnly: true, internal: true }, params!)
      scenes.push(
        ...data.map((d) => {
          d.project = project.name
          return d
        })
      )
    }

    for (let [index, _] of scenes.entries()) {
      scenes[index].thumbnailUrl += `?${Date.now()}`
    }

    return { data: scenes }
  }

  // @ts-ignore
  async get({ projectName, sceneName, metadataOnly }, params?: Params): Promise<{ data: SceneData }> {
    const project = await this.app.service('project').get(projectName, params)
    if (!project?.data) throw new Error(`No project named ${projectName} exists`)

    const sceneData = await getSceneData(projectName, sceneName, metadataOnly, params!.provider == null)

    return {
      data: sceneData
    }
  }

  async create(data: any, params?: Params): Promise<any> {
    const { projectName } = data
    logger.info('[scene.create]: ' + projectName)
    const storageProviderName = data.storageProviderName
    delete data.storageProviderName

    const storageProvider = getStorageProvider(storageProviderName)

    const project = await this.app.service('project').get(projectName, params)
    if (!project.data) throw new Error(`No project named ${projectName} exists`)

    const projectPath = `projects/${projectName}/`

    let newSceneName = NEW_SCENE_NAME
    let counter = 1

    while (true) {
      if (counter > 1) newSceneName = NEW_SCENE_NAME + '-' + counter
      if (!(await storageProvider.doesExist(`${newSceneName}.scene.json`, projectPath))) break

      counter++
    }

    await Promise.all(
      sceneAssetFiles.map((ext) =>
        storageProvider.moveObject(
          `default${ext}`,
          `${newSceneName}${ext}`,
          `projects/default-project`,
          projectPath,
          true
        )
      )
    )
    try {
      await storageProvider.createInvalidation(
        sceneAssetFiles.map((asset) => `projects/${projectName}/${newSceneName}${asset}`)
      )
    } catch (e) {
      logger.error(e)
      logger.info(sceneAssetFiles)
    }

    if (isDev) {
      const projectPathLocal = path.resolve(appRootPath.path, 'packages/projects/projects/' + projectName) + '/'
      for (const ext of sceneAssetFiles) {
        fs.copyFileSync(
          path.resolve(appRootPath.path, `packages/projects/default-project/default${ext}`),
          path.resolve(projectPathLocal + newSceneName + ext)
        )
      }
    }

    return { projectName, sceneName: newSceneName }
  }

  async patch(id: NullableId, data: RenameParams, params?: Params): Promise<any> {
    const { newSceneName, oldSceneName, projectName, storageProviderName } = data

    const storageProvider = getStorageProvider(storageProviderName)

    const project = await this.app.service('project').get(projectName, params)
    if (!project.data) throw new Error(`No project named ${projectName} exists`)

    const projectPath = `projects/${projectName}/`

    for (const ext of sceneAssetFiles) {
      const oldSceneJsonName = `${oldSceneName}${ext}`
      const newSceneJsonName = `${newSceneName}${ext}`

      if (await storageProvider.doesExist(oldSceneJsonName, projectPath)) {
        await storageProvider.moveObject(oldSceneJsonName, newSceneJsonName, projectPath, projectPath)
        try {
          await storageProvider.createInvalidation([projectPath + oldSceneJsonName, projectPath + newSceneJsonName])
        } catch (e) {
          logger.error(e)
          logger.info(projectPath + oldSceneJsonName, projectPath + newSceneJsonName)
        }
      }
    }

    if (isDev) {
      for (const ext of sceneAssetFiles) {
        const oldSceneJsonPath = path.resolve(
          appRootPath.path,
          `packages/projects/projects/${projectName}/${oldSceneName}${ext}`
        )
        if (fs.existsSync(oldSceneJsonPath)) {
          const newSceneJsonPath = path.resolve(
            appRootPath.path,
            `packages/projects/projects/${projectName}/${newSceneName}${ext}`
          )
          fs.renameSync(oldSceneJsonPath, newSceneJsonPath)
        }
      }
    }

    return
  }

  async update(projectName: string, data: UpdateParams, params?: Params): Promise<any> {
    const { sceneName, sceneData, thumbnailBuffer, storageProviderName } = data
    logger.info('[scene.update]: ', projectName, data)

    const storageProvider = getStorageProvider(storageProviderName)

    const project = await this.app.service('project').get(projectName, params)
    if (!project.data) throw new Error(`No project named ${projectName} exists`)

    const newSceneJsonPath = `projects/${projectName}/${sceneName}.scene.json`
    await storageProvider.putObject({
      Key: newSceneJsonPath,
      Body: Buffer.from(
        JSON.stringify(
          cleanSceneDataCacheURLs(sceneData ?? (defaultSceneSeed as unknown as SceneJson), storageProvider.cacheDomain)
        )
      ),
      ContentType: 'application/json'
    })

    if (thumbnailBuffer) {
      const sceneThumbnailPath = `projects/${projectName}/${sceneName}.thumbnail.jpeg`
      await storageProvider.putObject({
        Key: sceneThumbnailPath,
        Body: thumbnailBuffer as Buffer,
        ContentType: 'image/jpeg'
      })
    }

    try {
      await storageProvider.createInvalidation(
        sceneAssetFiles.map((asset) => `projects/${projectName}/${sceneName}${asset}`)
      )
    } catch (e) {
      logger.error(e)
      logger.info(sceneAssetFiles)
    }

    if (isDev) {
      const newSceneJsonPathLocal = path.resolve(
        appRootPath.path,
        `packages/projects/projects/${projectName}/${sceneName}.scene.json`
      )

      fs.writeFileSync(
        path.resolve(newSceneJsonPathLocal),
        JSON.stringify(
          cleanSceneDataCacheURLs(sceneData ?? (defaultSceneSeed as unknown as SceneJson), storageProvider.cacheDomain),
          null,
          2
        )
      )

      if (thumbnailBuffer) {
        const sceneThumbnailPath = path.resolve(
          appRootPath.path,
          `packages/projects/projects/${projectName}/${sceneName}.thumbnail.jpeg`
        )
        fs.writeFileSync(path.resolve(sceneThumbnailPath), thumbnailBuffer as Buffer)
      }
    }

    // return scene id for update hooks
    return { sceneId: `${projectName}/${sceneName}` }
  }

  // async patch(sceneId: NullableId, data: PatchData, params: Params): Promise<SceneDetailInterface> {}

  // @ts-ignore
  async remove(data, params?: Params): Promise<any> {
    const projectName = data.projectName
    const sceneName = data.sceneName
    const storageProviderName = data.storageProviderName
    const storageProvider = getStorageProvider(storageProviderName)

    const name = cleanString(sceneName)

    const project = await this.app.service('project').get(projectName, params)
    if (!project.data) throw new Error(`No project named ${projectName} exists`)

    for (const ext of sceneAssetFiles) {
      const assetFilePath = path.resolve(appRootPath.path, `packages/projects/projects/${projectName}/${name}${ext}`)
      if (fs.existsSync(assetFilePath)) {
        fs.rmSync(path.resolve(assetFilePath))
      }
    }

    await storageProvider.deleteResources(sceneAssetFiles.map((ext) => `projects/${projectName}/${name}${ext}`))

    try {
      await storageProvider.createInvalidation(
        sceneAssetFiles.map((asset) => `projects/${projectName}/${sceneName}${asset}`)
      )
    } catch (e) {
      logger.error(e)
      logger.info(sceneAssetFiles)
    }
  }
}
