import { mix } from '../mixin.js'

/** @import { MixinFunction } from '../mixin.js' */
/** @import { Fn } from './types.js' */

export const ProtectedConstructor = Symbol('ProtectedConstructor')
export class ProtectedConstructorError extends Error {
  /** @param {string} name */
  constructor(name) {
    super(`ProtectedConstructorError: ${name} cannot be directly constructed`)
  }
}

class Derivations {
  #inner

  /**
   * @param {Iterable<readonly [any, any]>} iterable 
   */
  constructor(iterable) {
    this.#inner = new Map(iterable)
  }

  /**
   * @param {Iterable<any>} keys 
   * @param {any} value 
   * @returns {this}
   */
  set(keys, value) {
    this.#inner.set(new Set(keys), value)
    return this
  }

  /**
   * @param {Iterable<any>} keys 
   * @returns {boolean}
   */
  has(keys) {
    return this.#inner.has(new Set(keys))
  }

  /**
   * @param {Iterable<any>} keys 
   */
  get(keys) {
    return this.#inner.get(new Set(keys))
  }

  /**
   * @param {Iterable<any>} keys 
   */
  delete(keys) {
    return this.#inner.delete(new Set(keys))
  }
}

export class NotImplementedError extends Error {
  /** @param {string} name */
  constructor(name) {
    super(`${name} is not implemented`)
  }
}

/** @typedef {(mixin: MixinFunction) => MixinFunction} MixinWrapper */

/**
 * @enum {number}
 */
const MethodType = Object.freeze({
  Instance: 0,
  Static: 1
})

class Method {
  #name

  /** @type {MethodType} */
  #type = MethodType.Instance

  /** @type {Fn} */
  #implementation

  /**
   * @param {string | Method} name 
   */
  constructor(name) {
    if (name instanceof Method) {
      return name
    }

    this.#name = name
  }

  /**
   * @param {string | Method} value 
   */
  static from(value) {
    return new Method(value)
  }

  isInstance() {
    this.#type = MethodType.Instance
    return this
  }

  isStatic() {
    this.#type = MethodType.Static
    return this
  }

  /** @param {Fn} f */
  implementation(f) {
    this.#implementation = f
    return this
  }

  /**
   * @param {string} interfaceName 
   */
  _defaultImplementation(interfaceName) {
    const err = new NotImplementedError(
      `${interfaceName}::${this.#name}`
    )

    return function() { throw err }
  }

  /**
   * @param {Function} target 
   */
  _getInstallationPoint(target) {
    switch (this.#type) {
      case 0: return target.prototype
      case 1: return target
      default: return target
    }
  }

  /**
   * @param {Interface} builder
   * @param {Function} target 
   */
  implement(builder, target) {

    const impl = this.#implementation || this._defaultImplementation(builder.name)

    this._getInstallationPoint(target)[this.#name] = impl
  }
}

class Interface {
  #name

  /** @type {Set<Method>} */
  #methods = new Set()

  /** @type {Set<Interface>} */
  #interfaces = new Set()

  /** @param {string} name */
  constructor(name) {
    this.#name = name
  }

  get name() {
    return this.#name
  }

  /**
   * @param {...(PropertyKey | Method)} methods
   * @returns {this}
   */
  specifies(...methods) {
    this.#methods = new Set(
      methods
        .map(Method.from)
        .concat(...this.#methods)
    )

    return this
  }

  /**
   * @param {...Interface} interfaces
   * @returns {this}
   */
  extends(...interfaces) {
    this.#interfaces = new Set(interfaces.concat(...this.#interfaces))
    return this
  }

  /**
   * @returns {MixinWrapper}
   */
  intoWrapper() {
    return this.build.bind(this)
  }

  /**
   * @param {FunctionConstructor} base
   */
  build(base) {
    const interfaces = [...this.#interfaces.values()].map(x => x.intoWrapper())

    const Interfaces = mix(base).with(...interfaces)

    const Algebra = class extends Interfaces { }

    for (const method of this.#methods) {
      method.implement(this, Algebra)
    }

    return Algebra
  }
}

/** @template T*/
class BaseSet {
  _value

  /** @param {T} value */
  constructor(value) {
    this._value = value
  }
}

/**
 * @param {PropertyKey} key
 * @param {object} obj
 * @returns {boolean}
 */
export const hasOwn = (key, obj) => Object.hasOwn(obj, key)

/**
 * @param {Function} left
 * @param {Function} right
 */
export const EitherOf = (left, right) => mix(left).with(l => right(l))

/**
 * @param {string} methodName
 * @param {(...args: any[]) => any} f
 * @param {object} obj
 */
export const apply = (methodName, f, obj) => {
  if (hasOwn(methodName, obj)) {
    return obj[methodName](f)
  } else {
    return f(obj)
  }
}

/**
 * @param {...Interface} algebras
 * @returns {FunctionConstructor}
 */
export const Algebra = (...algebras) => {
  return mix(BaseSet).with(...algebras.map(x => x.intoWrapper()))
}

export const Setoid = new Interface('Setoid')
  .specifies('equals')

export const Ord = new Interface('Ord')
  .specifies('lte')

export const Semigroupoid = new Interface('Semigroupoid')
  .specifies('compose')

export const Category = new Interface('Category')
  .extends(Semigroupoid)
  .specifies(
    Method.from('id').isStatic()
  )

export const Semigroup = new Interface('Semigroup')
  .specifies('concat')

export const Monoid = new Interface('Monoid')
  .extends(Semigroup)
  .specifies(
    Method.from('empty').isStatic()
  )

export const Group = new Interface('Group')
  .extends(Monoid)
  .specifies('invert')

export const Filterable = new Interface('Filterable')
  .specifies('filter')

export const Functor = new Interface('Functor')
  .specifies('map')

export const Contravariant = new Interface('Contravariant')
  .specifies('contramap')

export const Apply = new Interface('Apply')
  .extends(Functor)
  .specifies('ap')

export const Applicative = new Interface('Applicative')
  .extends(Apply)
  .specifies(
    Method.from('of').isStatic()
  )

export const Alt = new Interface('Alt')
  .extends(Functor)
  .specifies('alt')

export const Plus = new Interface('Plus')
  .extends(Alt)
  .specifies(
    Method.from('zero').isStatic()
  )

export const Alternative = new Interface('Alternative')
  .extends(Applicative, Plus)

export const Foldable = new Interface('Foldable')
  .specifies('fold')

export const Traversable = new Interface('Traversable')
  .extends(Functor, Foldable)
  .specifies('traverse')

export const Chain = new Interface('Chain')
  .extends(Apply)
  .specifies(
    Method.from('chain')
      .implementation(function(f) {
        return f(this._value)
      }))
  )

export const ChainRef = new Interface('ChainRec')
  .extends(Chain)
  .specifies(
    Method.from('chainRec').isStatic()
  )

export const Monad = new Interface('Monad')
  .extends(Applicative, Chain)

export const Extend = new Interface('Extend')
  .extends(Functor)
  .specifies('extend')

export const Comonad = new Interface('Comonad')
  .extends(Extend)
  .specifies('extract')

export const Bifunctor = new Interface('Bifunctor')
  .extends(Functor)
  .specifies('bimap')

export const Profunctor = new Interface('Profunctor')
  .extends(Functor)
  .specifies('promap')