import {isPlainObject} from 'lodash'
import {isCompound, JSONSchema, SchemaType} from './types/JSONSchema'

/**
 * Duck types a JSONSchema schema or property to determine which kind of AST node to parse it into.
 *
 * Due to what some might say is an oversight in the JSON-Schema spec, a given schema may
 * implicitly be an *intersection* of multiple JSON-Schema directives (ie. multiple TypeScript
 * types). The spec leaves it up to implementations to decide what to do with this
 * loosely-defined behavior.
 */
export function typesOfSchema(schema: JSONSchema): Set<SchemaType> {
  // tsType is an escape hatch that supercedes all other directives
  if (schema.tsType) {
    return new Set(['CUSTOM_TYPE'])
  }

  // Collect matched types
  const matchedTypes = new Set<SchemaType>()
  for (const [schemaType, f] of Object.entries(matchers)) {
    if (f(schema)) {
      matchedTypes.add(schemaType as SchemaType)
    }
  }

  // Default to an unnamed schema
  if (!matchedTypes.size) {
    matchedTypes.add('UNNAMED_SCHEMA')
  }

  return matchedTypes
}

const matchers: Record<SchemaType, (schema: JSONSchema) => boolean> = {
  ALL_OF(schema) {
    return 'allOf' in schema
  },
  ANY(schema) {
    if (Object.keys(schema).length === 0) {
      // The empty schema {} validates any value
      // @see https://json-schema.org/draft-07/json-schema-core.html#rfc.section.4.3.1
      return true
    }
    return schema.type === 'any'
  },
  ANY_OF(schema) {
    return 'anyOf' in schema
  },
  BOOLEAN(schema) {
    if ('enum' in schema) {
      return false
    }
    if (schema.type === 'boolean') {
      return true
    }
    if (!isCompound(schema) && typeof schema.default === 'boolean') {
      return true
    }
    return false
  },
  CUSTOM_TYPE() {
    return false // Explicitly handled before we try to match
  },
  NAMED_ENUM(schema) {
    return 'enum' in schema && 'tsEnumNames' in schema
  },
  NAMED_SCHEMA(schema) {
    // 8.2.1. The presence of "$id" in a subschema indicates that the subschema constitutes a distinct schema resource within a single schema document.
    return '$id' in schema && ('patternProperties' in schema || 'properties' in schema)
  },
  NEVER(schema: JSONSchema | boolean) {
    return schema === false
  },
  NULL(schema) {
    return schema.type === 'null'
  },
  NUMBER(schema) {
    if ('enum' in schema) {
      return false
    }
    if (schema.type === 'integer' || schema.type === 'number') {
      return true
    }
    if (!isCompound(schema) && typeof schema.default === 'number') {
      return true
    }
    return false
  },
  OBJECT(schema) {
    return (
      schema.type === 'object' &&
      !isPlainObject(schema.additionalProperties) &&
      !schema.allOf &&
      !schema.anyOf &&
      !schema.oneOf &&
      !schema.patternProperties &&
      !schema.properties &&
      !schema.required
    )
  },
  ONE_OF(schema) {
    return 'oneOf' in schema
  },
  REFERENCE(schema) {
    return '$ref' in schema
  },
  STRING(schema) {
    if ('enum' in schema) {
      return false
    }
    if (schema.type === 'string') {
      return true
    }
    if (!isCompound(schema) && typeof schema.default === 'string') {
      return true
    }
    return false
  },
  TYPED_ARRAY(schema) {
    if (schema.type && schema.type !== 'array') {
      return false
    }
    return 'items' in schema
  },
  UNION(schema) {
    return Array.isArray(schema.type)
  },
  UNNAMED_ENUM(schema) {
    if ('tsEnumNames' in schema) {
      return false
    }
    if (
      schema.type &&
      schema.type !== 'boolean' &&
      schema.type !== 'integer' &&
      schema.type !== 'number' &&
      schema.type !== 'string'
    ) {
      return false
    }
    return 'enum' in schema
  },
  UNNAMED_SCHEMA() {
    return false // Explicitly handled as the default case
  },
  UNTYPED_ARRAY(schema) {
    return schema.type === 'array' && !('items' in schema)
  },
}
