/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
/**
 * @fileOverview
 * Directive Parser
 */

import { bind, typof } from '../util/index'
import Watcher from '../core/watcher'
import config from '../config'

const { nativeComponentMap } = config

const SETTERS = {
  attr: 'setAttr',
  style: 'setStyle',
  event: 'addEvent'
}

/**
 * apply the native component's options(specified by template.type)
 * to the template
 */
export function applyNaitveComponentOptions (template) {
  const { type } = template
  const options = nativeComponentMap[type]

  if (typeof options === 'object') {
    for (const key in options) {
      if (template[key] == null) {
        template[key] = options[key]
      }
      else if (typof(template[key]) === 'object' &&
        typof(options[key]) === 'object') {
        for (const subkey in options[key]) {
          if (template[key][subkey] == null) {
            template[key][subkey] = options[key][subkey]
          }
        }
      }
    }
  }
}

/**
 * bind all id, attr, classnames, style, events to an element
 */
export function bindElement (vm, el, template) {
  setId(vm, el, template.id, vm)
  setAttr(vm, el, template.attr)
  setClass(vm, el, template.classList)
  setStyle(vm, el, template.style)
  bindEvents(vm, el, template.events)
}

/**
 * bind all props to sub vm and bind all style, events to the root element
 * of the sub vm if it doesn't have a replaced multi-node fragment
 */
export function bindSubVm (vm, subVm, template, repeatItem) {
  subVm = subVm || {}
  template = template || {}

  const options = subVm._options || {}

  // bind props
  let props = options.props

  if (Array.isArray(props)) {
    props = props.reduce((result, value) => {
      result[value] = true
      return result
    }, {})
  }

  mergeProps(repeatItem, props, vm, subVm)
  mergeProps(template.attr, props, vm, subVm)
}

/**
 * merge class and styles from vm to sub vm.
 */
export function bindSubVmAfterInitialized (vm, subVm, template, target = {}) {
  mergeClassStyle(template.classList, vm, subVm)
  mergeStyle(template.style, vm, subVm)

  // bind subVm to the target element
  if (target.children) {
    target.children[target.children.length - 1]._vm = subVm
  }
  else {
    target._vm = subVm
  }
}

/**
 * Bind props from vm to sub vm and watch their updates.
 */
function mergeProps (target, props, vm, subVm) {
  if (!target) {
    return
  }
  for (const key in target) {
    if (!props || props[key]) {
      const value = target[key]
      if (typeof value === 'function') {
        const returnValue = watch(vm, value, function (v) {
          subVm[key] = v
        })
        subVm[key] = returnValue
      }
      else {
        subVm[key] = value
      }
    }
  }
}

/**
 * Bind style from vm to sub vm and watch their updates.
 */
function mergeStyle (target, vm, subVm) {
  for (const key in target) {
    const value = target[key]
    if (typeof value === 'function') {
      const returnValue = watch(vm, value, function (v) {
        if (subVm._rootEl) {
          subVm._rootEl.setStyle(key, v)
        }
      })
      subVm._rootEl.setStyle(key, returnValue)
    }
    else {
      if (subVm._rootEl) {
        subVm._rootEl.setStyle(key, value)
      }
    }
  }
}

/**
 * Bind class & style from vm to sub vm and watch their updates.
 */
function mergeClassStyle (target, vm, subVm) {
  const css = vm._options && vm._options.style || {}

  /* istanbul ignore if */
  if (!subVm._rootEl) {
    return
  }

  const className = '@originalRootEl'
  css[className] = subVm._rootEl.classStyle

  function addClassName (list, name) {
    if (typof(list) === 'array') {
      list.unshift(name)
    }
  }

  if (typeof target === 'function') {
    const value = watch(vm, target, v => {
      addClassName(v, className)
      setClassStyle(subVm._rootEl, css, v)
    })
    addClassName(value, className)
    setClassStyle(subVm._rootEl, css, value)
  }
  else if (target != null) {
    addClassName(target, className)
    setClassStyle(subVm._rootEl, css, target)
  }
}

/**
 * bind id to an element
 * each id is unique in a whole vm
 */
export function setId (vm, el, id, target) {
  const map = Object.create(null)

  Object.defineProperties(map, {
    vm: {
      value: target,
      writable: false,
      configurable: false
    },
    el: {
      get: () => el || target._rootEl,
      configurable: false
    }
  })

  if (typeof id === 'function') {
    const handler = id
    id = handler.call(vm)
    if (id || id === 0) {
      vm._ids[id] = map
    }
    watch(vm, handler, (newId) => {
      if (newId) {
        vm._ids[newId] = map
      }
    })
  }
  else if (id && typeof id === 'string') {
    vm._ids[id] = map
  }
}

/**
 * bind attr to an element
 */
function setAttr (vm, el, attr) {
  bindDir(vm, el, 'attr', attr)
}

function setClassStyle (el, css, classList) {
  if (typeof classList === 'string') {
    classList = classList.split(/\s+/)
  }
  classList.forEach((name, i) => {
    classList.splice(i, 1, ...name.split(/\s+/))
  })
  const classStyle = {}
  const length = classList.length

  for (let i = 0; i < length; i++) {
    const style = css[classList[i]]
    if (style) {
      Object.keys(style).forEach((key) => {
        classStyle[key] = style[key]
      })
    }
  }
  el.setClassStyle(classStyle)
}

/**
 * bind classnames to an element
 */
function setClass (vm, el, classList) {
  if (typeof classList !== 'function' && !Array.isArray(classList)) {
    return
  }
  if (Array.isArray(classList) && !classList.length) {
    el.setClassStyle({})
    return
  }

  const style = vm._options && vm._options.style || {}
  if (typeof classList === 'function') {
    const value = watch(vm, classList, v => {
      setClassStyle(el, style, v)
    })
    setClassStyle(el, style, value)
  }
  else {
    setClassStyle(el, style, classList)
  }
}

/**
 * bind style to an element
 */
function setStyle (vm, el, style) {
  bindDir(vm, el, 'style', style)
}

/**
 * add an event type and handler to an element and generate a dom update
 */
function setEvent (vm, el, type, handler) {
  el.addEvent(type, bind(handler, vm))
}

/**
 * add all events of an element
 */
function bindEvents (vm, el, events) {
  if (!events) {
    return
  }
  const keys = Object.keys(events)
  let i = keys.length
  while (i--) {
    const key = keys[i]
    let handler = events[key]
    if (typeof handler === 'string') {
      handler = vm[handler]
      /* istanbul ignore if */
      if (!handler) {
        console.warn(`[JS Framework] The event handler "${handler}" is not defined.`)
      }
    }
    setEvent(vm, el, key, handler)
  }
}

/**
 * set a series of members as a kind of an element
 * for example: style, attr, ...
 * if the value is a function then bind the data changes
 */
function bindDir (vm, el, name, data) {
  if (!data) {
    return
  }
  const keys = Object.keys(data)
  let i = keys.length
  while (i--) {
    const key = keys[i]
    const value = data[key]
    if (typeof value === 'function') {
      bindKey(vm, el, name, key, value)
    }
    else {
      el[SETTERS[name]](key, value)
    }
  }
}

/**
 * bind data changes to a certain key to a name series in an element
 */
function bindKey (vm, el, name, key, calc) {
  const methodName = SETTERS[name]
  // watch the calc, and returns a value by calc.call()
  const value = watch(vm, calc, (value) => {
    function handler () {
      el[methodName](key, value)
    }
    const differ = vm && vm._app && vm._app.differ
    if (differ) {
      differ.append('element', el.depth || 0, el.ref, handler)
    }
    else {
      handler()
    }
  })

  el[methodName](key, value)
}

/**
 * watch a calc function and callback if the calc value changes
 */
export function watch (vm, calc, callback) {
  if (vm._static) {
    return calc.call(vm, vm)
  }
  const watcher = new Watcher(vm, calc, function (value, oldValue) {
    /* istanbul ignore if */
    if (typeof value !== 'object' && value === oldValue) {
      return
    }
    callback(value)
  })

  return watcher.value
}
