import URL from 'url-parse'
import Analytics from './modules/analytics'
import Core from './modules/core'
import Events from './modules/events'
import Media from './modules/media'
import Media2 from './modules/media2'
import Ptz from './modules/ptz'
import * as Util from './utils/util'

const MODULE_MAP = {
  analytics: './modules/analytics',
  core: './modules/core',
  events: './modules/events',
  media: './modules/media',
  media2: './modules/media2',
  ptz: './modules/ptz'
}

const MODULE_MAP_AFTER = {
  core: function (this: Camera) {
    this.core.init(this.serviceAddress, this.username, this.password)
  },

  media: function (this: Camera) {
    this.media.init(this.timeDiff, this.serviceAddress, this.username, this.password)
  }

  // snapshot: function (this: Camera) {
  //   const defaultProfile = this.getDefaultProfile()
  //   if (defaultProfile) {
  //     const snapshotUri = defaultProfile.SnapshotUri.Uri
  //     this.snapshot.init(snapshotUri, this.username, this.password)
  //   }
  // }
}

/**
 * Wrapper class for all onvif modules to manage an Onvif device (camera).
 */
export default class Camera {
  core?: Core
  analytics?: Analytics

  events?: Events
  media?: Media // Onvif 1.x
  media2?: Media2 // Onvif 2.x
  ptz?: Ptz

  rootPath = null
  serviceAddress: URL = null
  timeDiff = 0
  address = null
  port = null
  username = null
  password = null

  deviceInformation = null
  profileList = []
  defaultProfile = null

  /**
   * Add a module to Camera. The available modules are:
   * <ul>
   * <li>analytics - automatically added based on capabilities</li>
   * <li>core - automatically added</li>
   * <li>events - automatically added based on capabilities</li>
   * <li>media - automatically added based on capabilities</li>
   * <li>media2</li>
   * <li>ptz - automatically added based on capabilities</li>
   * </ul>
   * @param {string} name The name of the module.
   */
  async add(name: string) {
    const mod = MODULE_MAP[name]

    if (!MODULE_MAP[name]) {
      throw new Error(`Module '${name}' does not exist. Cannot add to Camera.`)
    }

    if (this[name]) {
      return
    }

    const { default: Inst } = await import(mod)

    // console.log(`Module ${name} instance:`, Inst)
    // return process.exit(0)

    const after = MODULE_MAP_AFTER[name] || (() => {})

    this[name] = new Inst()

    // console.log({ Inst })
    // console.log({ [name]: this[name] })
    // process.exit(0)

    after.call(this)
  }

  /**
   * Connect to the specified camera.
   * @param address The camera's address
   * @param port Optional port (80 used if this is null)
   * @param username The username for the account on the camera. This is optional if your camera does not require a username.
   * @param password The password for the account on the camera. This is optional if your camera does not require a password.
   * @param servicePath The service path for the camera. If null or 'undefined' the default path according to the ONVIF spec will be used.
   */
  connect(address: string, port?: number, username?: string, password?: string, servicePath?: string) {
    return new Promise(async (resolve, reject) => {
      // check for valid address
      let errMsg = ''

      if ((errMsg = Util.isInvalidValue(address, 'string'))) {
        return reject(new Error('The "address" argument for connect is invalid: ' + errMsg))
      }

      // provide defaults if not provided
      port = port || 80
      username = username || null
      password = password || null
      servicePath = servicePath || '/onvif/device_service'

      this.address = address
      this.port = port

      this.setAuth(username, password)

      // set up the service address
      let serviceAddress = 'http://' + address
      if (port && port !== 80) {
        serviceAddress = serviceAddress + ':' + port
      }

      this.rootPath = serviceAddress

      serviceAddress = serviceAddress + servicePath
      this.serviceAddress = new URL(serviceAddress)

      // add core module

      try {
        await this.add('core')
      } catch (err) {
        return reject(err)
      }

      try {
        await this.coreGetSystemDateAndTime()

        await this.coreGetServices()

        await this.coreGetCapabilities()

        await this.coreGetDeviceInformation()

        await this.mediaGetProfiles()

        await this.mediaGetStreamURI()

        await this.mediaGetSnapshotUri()

        await this.coreGetScopes()
      } catch (error) {
        console.error(error)
      }

      return resolve(this.getInformation())
    })
  }

  /**
   * Change or remove authentication.
   * @param username The username for the account on the camera. This is optional if your camera does not require a username.
   * @param password The password for the account on the camera. This is optional if your camera does not require a password.
   */
  setAuth(username?: string, password?: string) {
    this.username = username ?? null
    this.password = password ?? null
  }

  /**
   * Returns the ONVIF device's informaton. Available only after connection.
   */
  getInformation() {
    const info = this.deviceInformation
    if (info) {
      return JSON.parse(JSON.stringify(info))
    }
    return null
  }

  /**
   * Returns the default profile that will be used when one is not supplied to functions that require it. Available after connection.
   */
  getDefaultProfile() {
    return this.defaultProfile
  }

  coreGetSystemDateAndTime() {
    return new Promise<void>((resolve, reject) => {
      this.core
        .getSystemDateAndTime()
        .then(results => {
          this.timeDiff = this.core.getTimeDiff()
          resolve()
        })
        .catch(error => {
          console.error(error)
          reject(error)
        })
    })
  }

  coreGetServices() {
    return new Promise<void>((resolve, reject) => {
      this.core
        .getServices(true)
        .then(async results => {
          if (!('data' in results)) {
            return reject(new Error('No "data" field in core.getServices'))
          }

          const response = results.data.GetServicesResponse
          const services: any[] = response.Service

          // the appropriate modules will be automatically added
          // to camera based on the onvif device's services.
          // if GetServics is not supported, the GetCapabilities
          // fallback will be used.

          try {
            await Promise.all(
              services.map(async service => {
                this.checkForProxy(service)
                const namespace = service.Namespace
                if (namespace === 'http://www.onvif.org/ver10/device/wsdl') {
                  this.core.version = service.Version
                } else if (namespace === 'http://www.onvif.org/ver10/media/wsdl') {
                  await this.add('media')

                  if (this.media) {
                    this.media.init(
                      this.timeDiff,
                      Util.URLWithEnforcedPort(service.XAddr, this.port),
                      this.username,
                      this.password
                    )
                    this.media.version = service.Version
                  }
                } else if (namespace === 'http://www.onvif.org/ver10/events/wsdl') {
                  await this.add('events')

                  if (this.events) {
                    this.events.init(
                      this.timeDiff,
                      Util.URLWithEnforcedPort(service.XAddr, this.port),
                      this.username,
                      this.password
                    )
                    this.events.version = service.Version
                  }
                } else if (namespace === 'http://www.onvif.org/ver20/ptz/wsdl') {
                  await this.add('ptz')

                  if (this.ptz) {
                    this.ptz.init(
                      this.timeDiff,
                      Util.URLWithEnforcedPort(service.XAddr, this.port),
                      this.username,
                      this.password
                    )
                    this.ptz.version = service.Version
                  }
                } else if (namespace === 'http://www.onvif.org/ver20/analytics/wsdl') {
                  await this.add('analytics')

                  if (this.analytics) {
                    this.analytics.init(
                      this.timeDiff,
                      Util.URLWithEnforcedPort(service.XAddr, this.port),
                      this.username,
                      this.password
                    )
                    this.analytics.version = service.Version
                  }
                }
              })
            )

            resolve()
          } catch (err) {
            reject(err)
          }
        })
        .catch(error => {
          console.error(error)
          // don't fail because this isn't supported by the camera
          // spec says to use fallback of GetCapabilities method.
          resolve()
        })
    })
  }

  // make sure the serviceAddress matches
  // if not, then we may be behind a proxy and it needs
  // do be dealt with
  checkForProxy(service: { XAddr: string }) {
    const xaddrPath = new URL(service.XAddr)
    if (xaddrPath.hostname === this.serviceAddress.hostname) {
      // no proxy
      return
    }
    // build new path
    service.XAddr = this.rootPath + xaddrPath.pathname
  }

  coreGetCapabilities() {
    return new Promise<void>((resolve, reject) => {
      this.core
        .getCapabilities()
        .then(async results => {
          if (!('data' in results)) {
            return reject(new Error('No "data" field in core.getCapabilities'))
          }

          const c = results.data.GetCapabilitiesResponse.Capabilities

          if (!c) {
            return reject(new Error('Failed to initialize the device: No capabilities were found.'))
          }

          try {
            // the appropriate modules will be automatically added
            // to camera based on the onvif device's capabilities.
            if ('Analytics' in c) {
              const analytics = c.Analytics
              this.checkForProxy(analytics)
              if (analytics && 'XAddr' in analytics) {
                if (!this.analytics) {
                  await this.add('analytics')
                  if (this.analytics) {
                    const serviceAddress = Util.URLWithEnforcedPort(analytics.XAddr, this.port)
                    this.analytics.init(this.timeDiff, serviceAddress, this.username, this.password)
                  }
                }
                if (this.analytics) {
                  if ('RuleSupport' in analytics && analytics.RuleSupport === 'true') {
                    this.analytics.ruleSupport = true
                  }
                  if ('AnalyticsModuleSupport' in analytics && analytics.AnalyticsModuleSupport === 'true') {
                    this.analytics.analyticsModuleSupport = true
                  }
                }
              }
            }

            if ('Events' in c) {
              const events = c.Events
              this.checkForProxy(events)
              if (events && 'XAddr' in events) {
                if (!this.events) {
                  await this.add('events')
                  if (this.events) {
                    const serviceAddress = Util.URLWithEnforcedPort(events.XAddr, this.port)
                    this.events.init(this.timeDiff, serviceAddress, this.username, this.password)
                  }
                }
                if (this.events && this.analytics) {
                  if ('WSPullPointSupport' in events && events.WSPullPointSupport === 'true') {
                    this.analytics.wsPullPointSupport = true
                  }
                  if ('WSSubscriptionPolicySupport' in events && events.WSSubscriptionPolicySupport === 'true') {
                    this.analytics.wsSubscriptionPolicySupport = true
                  }
                }
              }
            }

            if ('Media' in c) {
              const media = c.Media
              this.checkForProxy(media)
              if (media && 'XAddr' in media) {
                if (!this.media) {
                  await this.add('media')
                  if (this.media) {
                    const serviceAddress = Util.URLWithEnforcedPort(media.XAddr, this.port)
                    this.media.init(this.timeDiff, serviceAddress, this.username, this.password)
                  }
                }
              }
            }

            if ('PTZ' in c) {
              const ptz = c.PTZ
              this.checkForProxy(ptz)
              if (ptz && 'XAddr' in ptz) {
                if (!this.ptz) {
                  await this.add('ptz')
                  if (this.ptz) {
                    const serviceAddress = Util.URLWithEnforcedPort(ptz.XAddr, this.port)
                    this.ptz.init(this.timeDiff, serviceAddress, this.username, this.password)
                  }
                }
              }
            }

            resolve()
          } catch (err) {
            reject(err)
          }
        })
        .catch(error => {
          console.error(error)
          reject(error)
        })
    })
  }

  coreGetDeviceInformation() {
    return new Promise<void>((resolve, reject) => {
      this.core
        .getDeviceInformation()
        .then(results => {
          if (!('data' in results)) {
            return reject(new Error('No "data" field in core.getDeviceInformation'))
          }
          this.deviceInformation = results.data.GetDeviceInformationResponse
          resolve()
        })
        .catch(error => {
          console.error(error)
          reject(error)
        })
    })
  }

  coreGetScopes() {
    return new Promise<void>((resolve, reject) => {
      this.core
        .getScopes()
        .then(results => {
          if (!('data' in results)) {
            return reject(new Error('No "data" field in core.getScopes'))
          }

          const scopes =
            typeof results.data.GetScopesResponse.Scopes === 'undefined' ||
            !Array.isArray(results.data.GetScopesResponse.Scopes)
              ? []
              : results.data.GetScopesResponse.Scopes
          this.deviceInformation.Ptz = false

          scopes.forEach(scope => {
            const s = scope.ScopeItem
            if (s.indexOf('onvif://www.onvif.org/hardware/') === 0) {
              const hardware = s.split('/').pop()
              this.deviceInformation.Hardware = hardware
            } else if (s.indexOf('onvif://www.onvif.org/type/Streaming') === 0) {
              this.deviceInformation.Streaming = true
            } else if (s.indexOf('onvif://www.onvif.org/type/video_encoder') === 0) {
              this.deviceInformation.VideoEncoder = true
            } else if (s.indexOf('onvif://www.onvif.org/type/audio_encoder') === 0) {
              this.deviceInformation.AudiooEncoder = true
            } else if (s.indexOf('onvif://www.onvif.org/type/ptz') === 0) {
              this.deviceInformation.Ptz = true
            } else if (s.indexOf('onvif://www.onvif.org/Profile/S') === 0) {
              this.deviceInformation.ProfileS = true
            } else if (s.indexOf('onvif://www.onvif.org/Profile/C') === 0) {
              this.deviceInformation.ProfileC = true
            } else if (s.indexOf('onvif://www.onvif.org/Profile/G') === 0) {
              this.deviceInformation.ProfileG = true
            } else if (s.indexOf('onvif://www.onvif.org/Profile/Q') === 0) {
              this.deviceInformation.ProfileQ = true
            } else if (s.indexOf('onvif://www.onvif.org/Profile/A') === 0) {
              this.deviceInformation.ProfileA = true
            } else if (s.indexOf('onvif://www.onvif.org/Profile/T') === 0) {
              this.deviceInformation.ProfileT = true
            } else if (s.indexOf('onvif://www.onvif.org/location/country/') === 0) {
              const country = s.split('/').pop()
              this.deviceInformation.Country = country
            } else if (s.indexOf('onvif://www.onvif.org/location/city/') === 0) {
              const city = s.split('/').pop()
              this.deviceInformation.City = city
            } else if (s.indexOf('onvif://www.onvif.org/name/') === 0) {
              let name = s.split('/').pop()
              name = name.replace(/_/g, ' ')
              this.deviceInformation.Name = name
            }
          })

          resolve()
        })
        .catch(error => {
          console.error(error)
          reject(error)
        })
    })
  }

  mediaGetProfiles() {
    return new Promise<void>((resolve, reject) => {
      this.media
        .getProfiles()
        .then(results => {
          if (!('data' in results)) {
            return reject(new Error('No "data" field in media.getProfiles'))
          }

          const profiles = results.data.GetProfilesResponse.Profiles

          if (!profiles) {
            reject(new Error('Failed to initialize the device: The targeted device does not any media profiles.'))
            return
          }

          const profileList = this.parseProfiles(profiles)

          this.profileList = this.profileList.concat(profileList)
          resolve()
        })
        .catch(error => {
          console.error(error)
          reject(error)
        })
    })
  }

  parseProfiles(profiles: any | any[]) {
    const profileList = []

    // When a single profile is given 'profiles' is the single profile
    if (!Array.isArray(profiles)) {
      profiles = [profiles]
    }

    profiles.forEach(profile => {
      profileList.push(profile)
      if (!this.defaultProfile) {
        this.defaultProfile = profile
        if (this.ptz) {
          this.ptz.setDefaultProfileToken(profile.$.token)
        }
      }
    })

    return profileList
  }

  /**
   * Returns an array of profiles. Available after connection.
   * The profiles will contain media stream URIs and snapshot URIs for each profile.
   */
  getProfiles() {
    return this.profileList
  }

  mediaGetStreamURI() {
    return new Promise<void>((resolve, reject) => {
      const protocols = ['UDP', 'HTTP', 'RTSP']
      let profileIndex = 0
      let protocolIndex = 0
      const getStreamUri = () => {
        const profile = this.profileList[profileIndex]
        if (profile) {
          const protocol = protocols[protocolIndex]
          if (protocol) {
            const token = profile.$.token
            this.media
              .getStreamUri('RTP-Unicast', protocol as 'UDP' | 'RTSP' | 'HTTP', token)
              .then(results => {
                if (!('data' in results)) {
                  return reject(new Error('No "data" field in media.getStreamUri'))
                }
                profile.StreamUri = results.data.GetStreamUriResponse.MediaUri
                ++protocolIndex
                getStreamUri()
              })
              .catch(error => {
                console.error(error)
                ++protocolIndex
                getStreamUri()
              })
          } else {
            ++profileIndex
            protocolIndex = 0
            getStreamUri()
          }
        } else {
          resolve()
        }
      }
      getStreamUri()
    })
  }

  mediaGetSnapshotUri() {
    return new Promise<void>((resolve, reject) => {
      let profileIndex = 0
      const getSnapshotUri = () => {
        const profile = this.profileList[profileIndex]
        if (profile) {
          // this.media.getSnapshotUri(profile['token'])
          this.media
            .getSnapshotUri(profile.$.token)
            .then(results => {
              if (!('data' in results)) {
                return reject(new Error('No "data" field in media.getSnapshotUri'))
              }
              try {
                const service = {
                  XAddr: results.data.GetSnapshotUriResponse.MediaUri.Uri
                }
                this.checkForProxy(service)
                profile.SnapshotUri = results.data.GetSnapshotUriResponse.MediaUri
                profile.SnapshotUri.Uri = service.XAddr
              } catch (e) {}
              ++profileIndex
              getSnapshotUri()
            })
            .catch(error => {
              console.error(error)
              ++profileIndex
              getSnapshotUri()
            })
        } else {
          resolve()
        }
      }
      getSnapshotUri()
    })
  }
}
