import Vue, { PropType, VNode } from '../index'
import { ComponentOptions, Component } from '../index'
import { CreateElement } from '../vue'

interface MyComponent extends Vue {
  a: number
}

const option: ComponentOptions<MyComponent> = {
  data() {
    return {
      a: 123
    }
  }
}

// contravariant generic should use never
const anotherOption: ComponentOptions<never> = option
const componentType: Component = option

Vue.component('sub-component', {
  components: {
    a: Vue.component(''),
    b: {}
  }
})

Vue.component('prop-component', {
  props: {
    size: Number,
    name: {
      type: String,
      default: '0',
      required: true
    }
  },
  data() {
    return {
      fixedSize: this.size.toFixed(),
      capName: this.name.toUpperCase()
    }
  }
})

Vue.component('string-prop', {
  props: ['size', 'name'],
  data() {
    return {
      fixedSize: this.size.whatever,
      capName: this.name.isany
    }
  }
})

class User {
  private u = 1
}
class Cat {
  private u = 1
}

interface IUser {
  foo: string
  bar: number
}

interface ICat {
  foo: any
  bar: object
}
type ConfirmCallback = (confirm: boolean) => void

Vue.component('union-prop', {
  props: {
    cat: Object as PropType<ICat>,
    complexUnion: { type: [User, Number] as PropType<User | number> },
    kittyUser: Object as PropType<ICat & IUser>,
    callback: Function as PropType<ConfirmCallback>,
    union: [User, Number] as PropType<User | number>
  },
  data() {
    this.cat
    this.complexUnion
    this.kittyUser
    this.callback(true)
    this.union
    return {
      fixedSize: this.union
    }
  }
})

Vue.component('union-prop-with-no-casting', {
  props: {
    mixed: [RegExp, Array],
    object: [Cat, User],
    primitive: [String, Number],
    regex: RegExp
  },
  data() {
    this.mixed
    this.object
    this.primitive
    this.regex.compile
  }
})

Vue.component('prop-with-primitive-default', {
  props: {
    id: {
      type: String,
      default: () => String(Math.round(Math.random() * 10000000))
    }
  },
  created(): void {
    this.id
  }
})

Vue.component('component', {
  data() {
    this.$mount
    this.size
    return {
      a: 1
    }
  },
  props: {
    size: Number,
    name: {
      type: String,
      default: '0',
      required: true
    }
  },
  propsData: {
    msg: 'Hello'
  },
  computed: {
    aDouble(): number {
      return this.a * 2
    },
    aPlus: {
      get(): number {
        return this.a + 1
      },
      set(v: number) {
        this.a = v - 1
      },
      cache: false
    }
  },
  methods: {
    plus(): void {
      this.a++
      this.aDouble.toFixed()
      this.aPlus = 1
      this.size.toFixed()
    }
  },
  watch: {
    a: function (val: number, oldVal: number) {
      console.log(`new: ${val}, old: ${oldVal}`)
    },
    b: 'someMethod',
    c: {
      handler(val, oldVal) {
        this.a = val
      },
      deep: true
    },
    d: {
      handler: 'someMethod',
      immediate: true
    },
    e: [
      'handle1',
      function handle2 (val, oldVal) {},
      {
        handler: function handle3 (val, oldVal) {},
      }
    ],
  },
  el: '#app',
  template: '<div>{{ message }}</div>',
  render(createElement) {
    return createElement(
      'div',
      {
        attrs: {
          id: 'foo'
        },
        props: {
          myProp: 'bar'
        },
        directives: [
          {
            name: 'a',
            value: 'foo'
          }
        ],
        domProps: {
          innerHTML: 'baz'
        },
        on: {
          click: new Function()
        },
        nativeOn: {
          click: new Function()
        },
        class: {
          foo: true,
          bar: false
        },
        style: {
          color: 'red',
          fontSize: '14px'
        },
        key: 'myKey',
        ref: 'myRef',
        refInFor: true
      },
      [
        createElement(),
        createElement('div', 'message'),
        createElement(Vue.component('component')),
        createElement({} as ComponentOptions<Vue>),
        createElement({
          functional: true,
          render(c: CreateElement) {
            return createElement()
          }
        }),

        createElement(() => Vue.component('component')),
        createElement(() => ({} as ComponentOptions<Vue>)),
        createElement((resolve, reject) => {
          resolve({} as ComponentOptions<Vue>)
          reject()
        }),

        'message',

        [createElement('div', 'message')]
      ]
    )
  },
  renderError(createElement, err) {
    return createElement('pre', { style: { color: 'red' } }, err.stack)
  },
  staticRenderFns: [],

  beforeCreate() {
    ;(this as any).a = 1
  },
  created() {},
  beforeDestroy() {},
  destroyed() {},
  beforeMount() {},
  mounted() {},
  beforeUpdate() {},
  updated() {},
  activated() {},
  deactivated() {},
  errorCaptured(err, vm, info) {
    err.message
    vm.$emit('error')
    info.toUpperCase()
    return true
  },
  serverPrefetch() {
    return Promise.resolve()
  },

  directives: {
    a: {
      bind() {},
      inserted() {},
      update() {},
      componentUpdated() {},
      unbind() {}
    },
    b(el, binding, vnode, oldVnode) {
      el.textContent

      binding.name
      binding.value
      binding.oldValue
      binding.expression
      binding.arg
      binding.modifiers['modifier']
    }
  },
  components: {
    a: Vue.component(''),
    b: {} as ComponentOptions<Vue>
  },
  transitions: {},
  filters: {
    double(value: number) {
      return value * 2
    }
  },
  parent: new Vue(),
  mixins: [Vue.component(''), {} as ComponentOptions<Vue>],
  name: 'Component',
  extends: {} as ComponentOptions<Vue>,
  delimiters: ['${', '}']
})

Vue.component('custom-prop-type-function', {
  props: {
    callback: Function as PropType<(confirm: boolean) => void>
  },
  methods: {
    confirm() {
      this.callback(true)
    }
  }
})

Vue.component('provide-inject', {
  provide: {
    foo: 1
  },
  inject: {
    injectFoo: 'foo',
    injectBar: Symbol(),
    injectBaz: { from: 'baz' },
    injectQux: { default: 1 },
    injectQuux: { from: 'quuz', default: () => ({ value: 1 }) }
  }
})

Vue.component('provide-function', {
  provide: () => ({
    foo: 1
  })
})

Vue.component('component-with-slot', {
  render(h): VNode {
    return h('div', this.$slots.default)
  }
})

Vue.component('component-with-scoped-slot', {
  render(h) {
    interface ScopedSlotProps {
      msg: string
    }

    return h('div', [
      h('child', [
        // default scoped slot as children
        (props: ScopedSlotProps) => [h('span', [props.msg])]
      ]),
      h('child', {
        scopedSlots: {
          // named scoped slot as vnode data
          item: (props: ScopedSlotProps) => [h('span', [props.msg])]
        }
      }),
      h('child', [
        // return single VNode (will be normalized to an array)
        (props: ScopedSlotProps) => h('span', [props.msg])
      ]),
      h('child', {
        // Passing down all slots from parent
        scopedSlots: this.$scopedSlots
      }),
      h('child', {
        // Passing down single slot from parent
        scopedSlots: {
          default: this.$scopedSlots.default
        }
      })
    ])
  },
  components: {
    child: {
      render(this: Vue, h: CreateElement) {
        const defaultSlot = this.$scopedSlots['default']!({ msg: 'hi' })
        defaultSlot &&
          defaultSlot.forEach(vnode => {
            vnode.tag
          })
        return h('div', [
          defaultSlot,
          this.$scopedSlots['item']!({ msg: 'hello' })
        ])
      }
    }
  }
})

Vue.component('narrow-array-of-vnode-type', {
  render(h): VNode {
    const slot = this.$scopedSlots.default!({})
    if (typeof slot === 'string') {
      // <template slot-scope="data">bare string</template>
      return h('span', slot)
    } else if (Array.isArray(slot)) {
      // template with multiple children
      const first = slot[0]
      if (!Array.isArray(first) && typeof first !== 'string' && first) {
        return first
      } else {
        return h()
      }
    } else if (slot) {
      // <div slot-scope="data">bare VNode</div>
      return slot
    } else {
      // empty template, slot === undefined
      return h()
    }
  }
})

Vue.component('functional-component', {
  props: ['prop'],
  functional: true,
  inject: ['foo'],
  render(createElement, context) {
    context.props
    context.children
    context.slots()
    context.data
    context.parent
    context.scopedSlots
    context.listeners.click
    return createElement('div', {}, context.children)
  }
})

Vue.component('functional-component-object-inject', {
  functional: true,
  inject: {
    foo: 'foo',
    bar: Symbol(),
    baz: { from: 'baz' },
    qux: { default: 1 },
    quux: { from: 'quuz', default: () => ({ value: 1 }) }
  },
  render(h) {
    return h('div')
  }
})

Vue.component('functional-component-check-optional', {
  functional: true
})

Vue.component('functional-component-multi-root', {
  functional: true,
  render(h) {
    return [
      h('tr', [h('td', 'foo'), h('td', 'bar')]),
      h('tr', [h('td', 'lorem'), h('td', 'ipsum')])
    ]
  }
})

Vue.component('async-component', (resolve, reject) => {
  setTimeout(() => {
    resolve(Vue.component('component'))
  }, 0)
  return new Promise(resolve => {
    resolve({
      functional: true,
      render(h: CreateElement) {
        return h('div')
      }
    })
  })
})

Vue.component('functional-component-v-model', {
  props: ['foo'],
  functional: true,
  model: {
    prop: 'foo',
    event: 'change'
  },
  render(createElement, context) {
    return createElement('input', {
      on: {
        input: new Function()
      },
      domProps: {
        value: context.props.foo
      }
    })
  }
})

Vue.component('async-es-module-component', () => import('./es-module'))

Vue.component('directive-expression-optional-string', {
  render(createElement) {
    return createElement('div', {
      directives: [
        {
          name: 'has-expression',
          value: 2,
          expression: '1 + 1'
        },
        {
          name: 'no-expression',
          value: 'foo'
        }
      ]
    })
  }
})