/*
 * Copyright 2021 Juan Cruz Viotti
 *
 * Licensed 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.
 */

import {
  strict as assert
} from 'assert'

import {
  range
} from 'lodash'

import {
  JSONValue
} from '../json'

import {
  BaseEncodingDefinition
} from './base-encoding-definition'

import {
  EncodingType
} from '../encoder'

import {
  Encoding,
  getStates,
  getEncoding
} from './index'

import {
  TypedFloorOptions,
  TypedRoofOptions,
  TypedBoundedOptions,
  SizeTypedOptions
} from '../encoder/array/options'

import {
  UINT8_MAX
} from '../utils/limits'

import {
  generatePermutations
} from '../utils/permutation'

import {
  EnumEncodingNames,
  EnumEncoding,
  getEnumEncoding
} from './enum'

import {
  ArrayEncodingSchema,
  EncodingSchema
} from '../schema'

export interface FIXED_TYPED_ARRAY_ENCODING extends BaseEncodingDefinition {
  readonly type: EncodingType.Array;
  readonly encoding: 'FIXED_TYPED_ARRAY';
  readonly options: SizeTypedOptions;
}

export interface BOUNDED_8BITS_TYPED_LENGTH_PREFIX_ENCODING extends BaseEncodingDefinition {
  readonly type: EncodingType.Array;
  readonly encoding: 'BOUNDED_8BITS_TYPED_LENGTH_PREFIX';
  readonly options: TypedBoundedOptions;
}

export interface BOUNDED_TYPED_LENGTH_PREFIX_ENCODING extends BaseEncodingDefinition {
  readonly type: EncodingType.Array;
  readonly encoding: 'BOUNDED_TYPED_LENGTH_PREFIX';
  readonly options: TypedBoundedOptions;
}

export interface FLOOR_TYPED_LENGTH_PREFIX_ENCODING extends BaseEncodingDefinition {
  readonly type: EncodingType.Array;
  readonly encoding: 'FLOOR_TYPED_LENGTH_PREFIX';
  readonly options: TypedFloorOptions;
}

export interface ROOF_TYPED_LENGTH_PREFIX_ENCODING extends BaseEncodingDefinition {
  readonly type: EncodingType.Array;
  readonly encoding: 'ROOF_TYPED_LENGTH_PREFIX';
  readonly options: TypedRoofOptions;
}

export type ArrayEncodingNames =
  EnumEncodingNames |
  'FIXED_TYPED_ARRAY' |
  'BOUNDED_8BITS_TYPED_LENGTH_PREFIX' |
  'BOUNDED_TYPED_LENGTH_PREFIX' |
  'FLOOR_TYPED_LENGTH_PREFIX' |
  'ROOF_TYPED_LENGTH_PREFIX'

export type ArrayEncoding =
  EnumEncoding |
  FIXED_TYPED_ARRAY_ENCODING |
  BOUNDED_8BITS_TYPED_LENGTH_PREFIX_ENCODING |
  BOUNDED_TYPED_LENGTH_PREFIX_ENCODING |
  FLOOR_TYPED_LENGTH_PREFIX_ENCODING |
  ROOF_TYPED_LENGTH_PREFIX_ENCODING

export const getArrayStates = (schema: ArrayEncodingSchema): number | JSONValue[] => {
  if (typeof schema.maxItems === 'number' &&
    (typeof schema.items !== 'undefined' || typeof schema.prefixItems !== 'undefined')) {
    const choices: number | JSONValue[][] =
      range(0, schema.maxItems).reduce((accumulator: number | JSONValue[][], index: number) => {
        const states: number | JSONValue[] =
          getStates((schema.prefixItems ?? [])[index] ?? schema.items ?? {})

        if (Array.isArray(accumulator) && Array.isArray(states)) {
          accumulator.push(states)
        } else if (typeof accumulator !== 'number' && !Array.isArray(states)) {
          return states + accumulator.reduce((subaccumulator: number, choice: JSONValue[]) => {
            return subaccumulator + choice.length
          }, 0)
        } else if (!Array.isArray(accumulator)) {
          return accumulator + (Array.isArray(states) ? states.length : states)
        }

        return accumulator
      }, [])

    if (typeof choices === 'number') {
      return choices
    }

    return range(schema.minItems ?? 0, schema.maxItems + 1)
      .reduce((accumulator: JSONValue[][], maximum: number) => {
        return accumulator.concat(generatePermutations(...choices.slice(0, maximum)))
      }, [])
  }

  return Infinity
}

export const getArrayEncoding = (schema: ArrayEncodingSchema, level: number): ArrayEncoding => {
  const states: number | JSONValue[] = getArrayStates(schema)
  if (Array.isArray(states)) {
    return getEnumEncoding({
      enum: states
    }, level)
  }

  const prefixEncodings: Encoding[] =
    (schema.prefixItems ?? []).map((subschema: EncodingSchema) => {
      return getEncoding(subschema, level + 1)
    })

  // Canonicalizations rules ensure this
  assert(typeof schema.minItems === 'number')
  assert(schema.minItems >= 0)
  assert(typeof schema.items !== 'undefined')

  if (typeof schema.maxItems !== 'undefined' &&
    schema.minItems === 0 && schema.maxItems > UINT8_MAX) {
    return {
      type: EncodingType.Array,
      encoding: 'ROOF_TYPED_LENGTH_PREFIX',
      options: {
        maximum: schema.maxItems,
        prefixEncodings,
        encoding: getEncoding(schema.items, level + 1)
      }
    }
  }

  if (typeof schema.maxItems !== 'undefined' &&
    (schema.minItems !== 0 || schema.maxItems <= UINT8_MAX)) {
    return {
      type: EncodingType.Array,
      encoding: (schema.maxItems - schema.minItems <= UINT8_MAX)
        ? 'BOUNDED_8BITS_TYPED_LENGTH_PREFIX'
        : 'BOUNDED_TYPED_LENGTH_PREFIX',
      options: {
        minimum: schema.minItems,
        maximum: schema.maxItems,
        prefixEncodings,
        encoding: getEncoding(schema.items, level + 1)
      }
    }
  }

  return {
    type: EncodingType.Array,
    encoding: 'FLOOR_TYPED_LENGTH_PREFIX',
    options: {
      minimum: schema.minItems,
      prefixEncodings,
      encoding: getEncoding(schema.items, level + 1)
    }
  }
}
