const getNameFromSchema = require("./getNameFromSchema");
const unindent = require("./unindent");
const indent = require("./indent");
const makeIdentifier = require("./makeIdentifier");

function resolveProperty(
  schemaCache,
  config,
  parentName,
  propertyName,
  propertyDetails,
  required
) {
  if (Object.keys(propertyDetails).length === 0) {
    // Ignore totally empty properties.
    return undefined;
  }

  // If we don't know what's required, act as if everything is.
  // Specifically this means we _don't_ make it optional.
  const isRequired = required === undefined || required.includes(propertyName);
  const makeOptional = !isRequired && propertyDetails.default === undefined;

  if (propertyDetails.type == "array") {
    return resolveArray(
      schemaCache,
      config,
      parentName,
      propertyName,
      propertyDetails,
      required
    );
  } else if (propertyDetails.type == "integer") {
    return {
      ...propertyDefaults(propertyName, propertyDetails),
      headers: ["<cstdint>", ...(makeOptional ? ["<optional>"] : [])],
      type: makeOptional ? "std::optional<int64_t>" : "int64_t",
      readerHeaders: [`"CesiumJsonReader/IntegerJsonHandler.h"`],
      readerType: "CesiumJsonReader::IntegerJsonHandler<int64_t>",
      needsInitialization: !makeOptional
    };
  } else if (propertyDetails.type == "number") {
    return {
      ...propertyDefaults(propertyName, propertyDetails),
      headers: makeOptional ? ["<optional>"] : [],
      type: makeOptional ? "std::optional<double>" : "double",
      readerHeaders: [`"CesiumJsonReader/DoubleJsonHandler.h"`],
      readerType: "CesiumJsonReader::DoubleJsonHandler",
      needsInitialization: !makeOptional
    };
  } else if (propertyDetails.type == "boolean") {
    return {
      ...propertyDefaults(propertyName, propertyDetails),
      headers: makeOptional ? ["<optional>"] : [],
      type: makeOptional ? "std::optional<bool>" : "bool",
      readerHeaders: `"CesiumJsonReader/BoolJsonHandler.h"`,
      readerType: "CesiumJsonReader::BoolJsonHandler",
      needsInitialization: ~makeOptional
    };
  } else if (propertyDetails.type == "string") {
    return {
      ...propertyDefaults(propertyName, propertyDetails),
      type: makeOptional ? "std::optional<std::string>" : "std::string",
      headers: ["<string>", ...(makeOptional ? ["<optional>"] : [])],
      readerHeaders: [`"CesiumJsonReader/StringJsonHandler.h"`],
      readerType: "CesiumJsonReader::StringJsonHandler",
    };
  } else if (
    propertyDetails.type == "object" &&
    propertyDetails.additionalProperties
  ) {
    return resolveDictionary(
      schemaCache,
      config,
      parentName,
      propertyName,
      propertyDetails,
      required
    );
  } else if (
    propertyDetails.anyOf &&
    propertyDetails.anyOf.length > 0 &&
    propertyDetails.anyOf[0].enum
  ) {
    return resolveEnum(
      schemaCache,
      config,
      parentName,
      propertyName,
      propertyDetails,
      makeOptional
    );
  } else if (propertyDetails.$ref) {
    const itemSchema = schemaCache.load(propertyDetails.$ref);
    if (itemSchema.title === "glTF Id") {
      return {
        ...propertyDefaults(propertyName, propertyDetails),
        type: "int32_t",
        defaultValue: -1,
        headers: ["<cstdint>"],
        readerHeaders: [`"CesiumJsonReader/IntegerJsonHandler.h"`],
        readerType: "CesiumJsonReader::IntegerJsonHandler<int32_t>",
      };
    } else {
      const type = getNameFromSchema(config, itemSchema);
      const typeName = getNameFromSchema(config, itemSchema);

      return {
        ...propertyDefaults(propertyName, propertyDetails),
        type: makeOptional ? `std::optional<${typeName}>` : typeName,
        headers: [`"CesiumGltf/${type}.h"`, ...(makeOptional ? ["<optional>"] : [])],
        readerType: `${type}JsonHandler`,
        readerHeaders: [`"${type}JsonHandler.h"`],
        schemas: [itemSchema],
      };
    }
  } else if (propertyDetails.allOf && propertyDetails.allOf.length == 1) {
    const nested = resolveProperty(
      schemaCache,
      config,
      parentName,
      propertyName,
      propertyDetails.allOf[0],
      required,
    );

    return {
      ...nested,
      briefDoc: propertyDefaults(propertyName, propertyDetails).briefDoc,
      fullDoc: propertyDefaults(propertyName, propertyDetails).fullDoc,
    };
  } else {
    console.warn(`Skipping unhandled property ${propertyName}.`);
    return undefined;
  }
}

function toPascalCase(name) {
  if (name.length === 0) {
    return name;
  }

  return name[0].toUpperCase() + name.substr(1);
}

function propertyDefaults(propertyName, propertyDetails) {
  const fullDoc =
    propertyDetails.gltf_detailedDescription &&
    propertyDetails.gltf_detailedDescription.indexOf(
      propertyDetails.description
    ) === 0
      ? propertyDetails.gltf_detailedDescription
          .substr(propertyDetails.description.length)
          .trim()
      : propertyDetails.gltf_detailedDescription;
  return {
    name: propertyName,
    headers: [],
    readerHeaders: [],
    readerHeadersImpl: [],
    type: "",
    defaultValue: propertyDetails.default !== undefined ? propertyDetails.default.toString() : undefined,
    readerType: "",
    schemas: [],
    localTypes: [],
    readerLocalTypes: [],
    readerLocalTypesImpl: [],
    briefDoc: propertyDetails.description,
    fullDoc: fullDoc,
  };
}

function resolveArray(
  schemaCache,
  config,
  parentName,
  propertyName,
  propertyDetails,
  required
) {
  const itemProperty = resolveProperty(
    schemaCache,
    config,
    parentName,
    propertyName + ".items",
    propertyDetails.items,
    undefined
  );

  if (!itemProperty) {
    return undefined;
  }

  return {
    ...propertyDefaults(propertyName, propertyDetails),
    name: propertyName,
    headers: ["<vector>", ...itemProperty.headers],
    schemas: itemProperty.schemas,
    localTypes: itemProperty.localTypes,
    type: `std::vector<${itemProperty.type}>`,
    defaultValue: propertyDetails.default ? `{ ${propertyDetails.default} }` : undefined,
    readerHeaders: [`"CesiumJsonReader/ArrayJsonHandler.h"`, ...itemProperty.readerHeaders],
    readerType: `CesiumJsonReader::ArrayJsonHandler<${itemProperty.type}, ${itemProperty.readerType}>`,
  };
}

function resolveDictionary(
  schemaCache,
  config,
  parentName,
  propertyName,
  propertyDetails,
  required
) {
  const additional = resolveProperty(
    schemaCache,
    config,
    parentName,
    propertyName + ".additionalProperties",
    propertyDetails.additionalProperties,
    required
  );

  if (!additional) {
    return undefined;
  }

  return {
    ...propertyDefaults(propertyName, propertyDetails),
    name: propertyName,
    headers: ["<unordered_map>", ...additional.headers],
    schema: additional.schemas,
    localTypes: additional.localTypes,
    type: `std::unordered_map<std::string, ${additional.type}>`,
    readerHeaders: [`"CesiumJsonReader/DictionaryJsonHandler.h"`, ...additional.readerHeaders],
    readerType: `CesiumJsonReader::DictionaryJsonHandler<${additional.type}, ${additional.readerType}>`,
  };
}

/**
 * @brief Creates a documentation comment block for the given property.
 * 
 * The result will be a (non-indented) doxygen comment block that contains 
 * the `briefDoc` (or `name`) and `fullDoc` from the given property values.
 * 
 * @param {Object} propertyValues The property
 * @return {String} The comment block
 */
function createPropertyDoc(propertyValues) {
  let propertyDoc = `/**\n * @brief ${propertyValues.briefDoc || propertyValues.name}\n`;
  if (propertyValues.fullDoc) {
    propertyDoc += ` *\n * ${propertyValues.fullDoc.split("\n").join("\n * ")}\n`;
  }
  propertyDoc += ` */`;
  return propertyDoc;
}
 

function resolveEnum(
  schemaCache,
  config,
  parentName,
  propertyName,
  propertyDetails,
  makeOptional
) {
  if (
    !propertyDetails.anyOf ||
    propertyDetails.anyOf.length === 0 ||
    !propertyDetails.anyOf[0].enum ||
    propertyDetails.anyOf[0].enum.length === 0
  ) {
    return undefined;
  }

  const enumName = toPascalCase(propertyName);

  const readerTypes = createEnumReaderType(
    parentName,
    enumName,
    propertyName,
    propertyDetails
  );

  const propertyDefaultValues = propertyDefaults(propertyName, propertyDetails);
  const result = {
    ...propertyDefaultValues,
    localTypes: [
      unindent(`
        ${createPropertyDoc(propertyDefaultValues)}
        enum class ${toPascalCase(propertyName)} {
            ${indent(
              propertyDetails.anyOf
                .map((e) => createEnum(e))
                .filter((e) => e !== undefined)
                .join(",\n\n"),
              12
            )}
        };
      `),
    ],
    type: makeOptional ? `std::optional<${enumName}>` : enumName,
    headers: makeOptional ? ["<optional>"] : [],
    defaultValue: createEnumDefault(enumName, propertyDetails),
    readerHeaders: [`"CesiumGltf/${parentName}.h"`],
    readerLocalTypes: readerTypes,
    readerLocalTypesImpl: createEnumReaderTypeImpl(
      parentName,
      enumName,
      propertyName,
      propertyDetails
    ),
    needsInitialization: !makeOptional
  };

  if (readerTypes.length > 0) {
    result.readerType = `${enumName}JsonHandler`;
  } else {
    result.readerType = `CesiumJsonReader::IntegerJsonHandler<${parentName}::${enumName}>`;
    result.readerHeaders.push(`"CesiumJsonReader/IntegerJsonHandler.h"`);
  }

  return result;
}

function createEnumDefault(enumName, propertyDetails) {
  if (propertyDetails.default === undefined) {
    return undefined;
  }

  if (propertyDetails.anyOf[0].type === "integer") {
    return `${enumName}(${propertyDetails.default})`;
  } else {
    return `${enumName}::${propertyDetails.default}`;
  }
}

function createEnum(enumDetails) {
  if (!enumDetails.enum || enumDetails.enum.length === 0) {
    return undefined;
  }

  if (enumDetails.type === "integer") {
    return `${makeIdentifier(enumDetails.description)} = ${
      enumDetails.enum[0]
    }`;
  } else {
    return makeIdentifier(enumDetails.enum[0]);
  }
}

function createEnumReaderType(
  parentName,
  enumName,
  propertyName,
  propertyDetails
) {
  if (propertyDetails.anyOf[0].type === "integer") {
    // No special reader needed for integer enums.
    return [];
  }

  return unindent(`
    class ${enumName}JsonHandler : public CesiumJsonReader::JsonHandler {
    public:
      ${enumName}JsonHandler() noexcept : CesiumJsonReader::JsonHandler() {}
      void reset(CesiumJsonReader::IJsonHandler* pParent, ${parentName}::${enumName}* pEnum);
      virtual CesiumJsonReader::IJsonHandler* readString(const std::string_view& str) override;

    private:
      ${parentName}::${enumName}* _pEnum = nullptr;
    };
  `);
}

function createEnumReaderTypeImpl(
  parentName,
  enumName,
  propertyName,
  propertyDetails
) {
  if (propertyDetails.anyOf[0].type === "integer") {
    // No special reader needed for integer enums.
    return [];
  }

  return unindent(`
    void ${parentName}JsonHandler::${enumName}JsonHandler::reset(CesiumJsonReader::IJsonHandler* pParent, ${parentName}::${enumName}* pEnum) {
      JsonHandler::reset(pParent);
      this->_pEnum = pEnum;
    }

    CesiumJsonReader::IJsonHandler* ${parentName}JsonHandler::${enumName}JsonHandler::readString(const std::string_view& str) {
      using namespace std::string_literals;

      assert(this->_pEnum);

      ${indent(
        propertyDetails.anyOf
          .map((e) =>
            e.enum && e.enum[0] !== undefined
              ? `if ("${
                  e.enum[0]
                }"s == str) *this->_pEnum = ${parentName}::${enumName}::${makeIdentifier(
                  e.enum[0]
                )};`
              : undefined
          )
          .filter((s) => s !== undefined)
          .join("\nelse "),
        6
      )}
      else return nullptr;

      return this->parent();
    }
  `);
}

module.exports = resolveProperty;
