/*
 * Copyright (C) 2021 StApps
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation, version 3.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program. If not, see <https://www.gnu.org/licenses/>.
 */
import {Logger} from '@openstapps/logger';
import * as request from 'got';
import {
  expandTypeValue,
  isLightweightClass,
  LightweightAliasDefinition,
  LightweightClassDefinition,
  LightweightDefinition,
  LightweightProperty,
  LightweightType,
} from '@openstapps/easy-ast';
import {UMLConfig} from './uml-config.js';
import {writeFile} from 'fs/promises';

/**
 * Converts the lightweight class/enum definitions according to the configuration,
 * to valid PlantUML Code, which will then be encoded, converted by the plantuml server
 * and saved as a .svg file in directory, in which this method was called
 * @param definitions all type definitions of the project
 * @param config contains information on how the PlantUML should be generated
 * @param plantUmlBaseURL Hostname of the PlantUML-Server
 */
export async function createDiagram(
  definitions: LightweightDefinition[],
  config: UMLConfig,
  plantUmlBaseURL: string,
): Promise<string> {
  // when non definitions were specified use all
  config.definitions = definitions.map(it => it.name);

  // when providing definitions and either showing associations or inheritance the
  // inherited definitions will be added automatically
  if (config.showInheritance) {
    // TODO: showInheritance
    /*const inheritedDefinitions = gatherTypeAssociations(
      definitions,
      config.definitions,
    );*/
    // config.definitions = config.definitions.concat(inheritedDefinitions);
  }

  // creates a UML definition for every specified definition name
  // however if no definitions were provided all definitions will be transformed
  const modelPlantUMLCode = definitions
    .filter(it => !config.definitions.includes(it.name))
    .map(definition =>
      isLightweightClass(definition)
        ? createPlantUMLCodeForClass(config, definition)
        : createPlantUMLCodeForEnum(config, definition),
    )
    .join('');

  return createDiagramFromString(modelPlantUMLCode, plantUmlBaseURL, config.outputFileName);
}

/**
 * This will encode the plantuml code and post the code to the plantuml server
 * The server will then parse the code and create a corresponding diagram
 * @param modelPlantUMLCode raw PlantUML code
 * @param plantUmlBaseURL PlantUML server address that shall be used
 * @param outputFile filename of the output file without file extension
 */
export async function createDiagramFromString(
  modelPlantUMLCode: string,
  plantUmlBaseURL: string,
  outputFile = `Diagram-${new Date().toISOString()}`,
) {
  // @ts-expect-error no declarations
  const plantumlEncoder = await import('plantuml-encoder');
  const plantUMLCode = plantumlEncoder.encode(`@startuml\n${modelPlantUMLCode}\n@enduml`);
  const url = `${plantUmlBaseURL}/svg/${plantUMLCode}`;
  let response;
  try {
    response = await request.default.get(url);
    const httpOK = 200;
    if (response.statusCode !== httpOK) {
      await Logger.error(`Plantuml Server responded with an error.\n${response.statusMessage}`);
      throw new Error('Response not okay');
    }
  } catch (error) {
    Logger.log(
      `Please try using the public plantuml server:\nhttp://www.plantuml.com/plantuml/svg/${plantUMLCode}`,
    );
    throw error;
  }
  // attach file extension
  const fileName = `${outputFile.replaceAll(/[^\w-]/g, '_')}.svg`;

  await writeFile(fileName, response.body);
  Logger.log(`Writen data to file: ${fileName}`);

  return fileName;
}

/**
 * Recursively iterates over all types, to find implemented generic types and parents
 * @param definitions all type definitions of the project
 * @param abstractionNames currently known string values of inherited classes
 */

/*function gatherTypeAssociations(
  definitions: LightweightDefinition[],
  abstractionNames: string[],
): string[] {
  let abstractions: string[] = [];
  for (const name of abstractionNames) {
    const declaration = definitions.find(
      (definition) => definition.name === name,
    );
    if (isLightweightClass(declaration)) {
      const currentAbstractions: string[] = declaration.extendedDefinitions.concat(
        declaration.implementedDefinitions,
      );

      abstractions = abstractions.concat(currentAbstractions);
      abstractions = abstractions.concat(
        gatherTypeAssociations(definitions, currentAbstractions),
      );
    }
  }

  return abstractions;
}*/

/**
 * Collects all reference information of this type.
 *
 * Reference information is everything that is indirectly referencing a type or class by name.
 * @param type Type with references to other types
 */
function getReferenceTypes(type: LightweightType): string[] {
  const types: string[] = [];
  if (type.referenceName !== undefined) {
    types.push(type.referenceName);
  }

  if (type.genericsTypes) {
    for (const specificType of type.genericsTypes) {
      for (const value of getReferenceTypes(specificType)) {
        types.push(value);
      }
    }
  }

  if (Array.isArray(type.specificationTypes)) {
    for (const specificType of type.specificationTypes) {
      for (const value of getReferenceTypes(specificType)) {
        types.push(value);
      }
    }
  }

  return types;
}

/**
 * Creates Plant UML code according to the config for the provided class
 * @param config Configuration for how the UML should be tweaked
 * @param readerClass Class or interface representation
 */
function createPlantUMLCodeForClass(config: UMLConfig, readerClass: LightweightClassDefinition): string {
  // create the definition header, what type the definition is, it's name and it's inheritance
  let model = `${readerClass.modifiers} ${readerClass.name}`;

  if (readerClass.typeParameters?.length ?? 0 > 0) {
    model += `<${readerClass.typeParameters!.join(', ')}>`;
  }

  if (config.showInheritance && (readerClass.extendedDefinitions?.length ?? 0 > 0)) {
    // PlantUML will automatically create links, when using extends
    model += ` extends ${readerClass.extendedDefinitions!.join(', ')}`;
  }
  if (config.showInheritance && (readerClass.implementedDefinitions?.length ?? 0 > 0)) {
    // PlantUML will automatically create links, when using implements
    model += ` implements ${readerClass.implementedDefinitions!.join(', ')}`;
  }
  model += '{';

  // add the properties to the definition body
  if (config.showProperties && readerClass.properties) {
    for (const key in readerClass.properties) {
      const property = readerClass.properties[key];
      if (property.optional && !config.showOptionalProperties) {
        // don't show optional attributes
        continue;
      }
      /*if (property.inherited && !config.showInheritedProperties) {
        // don't show inherited properties
        continue;
      }*/
      model += `\n\t${createPropertyLine(property)}`;
    }
  }

  // close the definition body
  model += '\n}\n';

  // add associations from properties with references
  if (readerClass.properties) {
    for (const key in readerClass.properties) {
      const property = readerClass.properties[key];
      const types: string[] = getReferenceTypes(property.type);
      for (const type of types) {
        if (config.showAssociations) {
          /*if (property.inherited && !config.showInheritedProperties) {
            continue;
          }*/
          model += `${readerClass.name} -up-> ${type} : ${property.name} >\n`;
        }
      }
    }
  }

  return model;
}

/**
 * Creates PlantUML code according to the config for the provided enum/-like definition
 * @param config Configuration for how the UML should be tweaked
 * @param readerEnum Enum/-like representation
 */
function createPlantUMLCodeForEnum(config: UMLConfig, readerEnum: LightweightAliasDefinition): string {
  // create enum header
  let model = `enum ${readerEnum.name} {`;
  // add values
  if (config.showEnumValues && readerEnum.type?.specificationTypes) {
    for (const value of readerEnum.type?.specificationTypes) {
      model += `\n\t${value.toString()}`;
    }
  }
  model += '\n}\n';

  return model;
}

/**
 * Creates a property PlantUML Line
 */
function createPropertyLine(property: LightweightProperty): string {
  const prefix = `${/*(property.inherited ? '/ ' : */ ''}${property.optional ? '? ' : ''}`;

  return `${prefix}${property.name} : ${expandTypeValue(property.type)}`;
}
