/**
 *
 * Copyright (c) 2024 Analog Devices, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 *
 * 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 {Command, Flags} from '@oclif/core';
import {promises as fs} from 'node:fs';
import path from 'node:path';

import type {Configdata} from '../types/configdata.js';

import {getSoc} from '../lib/socs.js';

export default class PinconfigGenerate extends Command {
  static description =
    'Generate source code from the provided .cfsconfig file.';

  static flags = {
    engine: Flags.string({
      char: 'e',
      default: 'msdk',
      summary: 'Set the code generation engine.'
    }),
    input: Flags.file({
      char: 'i',
      required: true,
      summary: 'Set the .cfsconfig file.'
    }),
    output: Flags.directory({
      char: 'o',
      default: '.',
      summary: 'Set the output directory for generated code.'
    }),
    verbose: Flags.boolean({
      char: 'v',
      summary: 'Display full paths for generated code.'
    }),
    preview: Flags.boolean({
      char: 'p',
      summary: 'Display generated code on stdout.'
    }),
    format: Flags.string({
      char: 'f',
      default: 'text',
      summary: 'Set the output format for preview.',
      options: ['text', 'json']
    }),
    force: Flags.boolean({
      default: false,
      summary: 'Forces file overwrites and folder creation.'
    }),
    list: Flags.boolean({
      default: false,
      summary:
        'List the filenames that will be generated by the generate command.'
    }),
    file: Flags.string({
      summary: 'Generate only the specified file.'
    })
  };

  formatFileContent(
    formatFlag: string,
    fileContent: string[]
  ): string {
    return formatFlag === 'json'
      ? JSON.stringify(fileContent, null, 2)
      : fileContent.join('\n') + '\n';
  }

  formatTextContent(fileName: string, fileContent: string[]): string {
    return `File: ${fileName}\nContent:\n${fileContent.join('\n')}\n`;
  }

  /* eslint-disable complexity */
  async run() {
    const {flags} = await this.parse(PinconfigGenerate);

    // read input file
    const inputJson = await fs
      .readFile(flags.input, 'utf8')
      .catch((error) => {
        throw new Error(
          `Could not read input file "${flags.input}".\n${error}`
        );
      });

    // parse input file
    let input;
    try {
      input = JSON.parse(inputJson);
    } catch (error) {
      throw new Error(
        `Could not parse input file "${flags.input}", maybe not json?\n${error}`
      );
    }

    const configdata = input as Configdata;

    const isZephyrProject =
      configdata.FirmwarePlatform.toLowerCase().includes('zephyr');
    const engineIsZephyr = flags.engine === 'zephyr';

    if (isZephyrProject !== engineIsZephyr) {
      this.error(
        `The configuration file's data and the specified code generation engine must be consistent. For example, ensure that if the configuration file is for a Zephyr project, the engine is set to 'zephyr'.`
      );
    }

    // ensure there are no pin conflicts
    const isPinUsed = {} as Record<string, boolean>;
    for (const pin of configdata.Pins) {
      if (isPinUsed[pin.Pin]) {
        this.error(
          `This configuration contains a conflict for pin ${pin.Pin}.`
        );
      }

      isPinUsed[pin.Pin] = true;
    }

    const soc = await getSoc(
      this.config,
      configdata.Soc.toLowerCase() +
        '-' +
        configdata.Package.toLowerCase()
    );

    // generate code
    let outputFiles;
    const result = await this.config.runHook('generate-code', {
      engine: flags.engine,
      configdata,
      soc
    });

    if (result.failures.length > 0) {
      this.error(result.failures[0].error.message);
    }

    for (const success of result.successes) {
      if (success.result) {
        if (outputFiles) {
          this.warn(
            `Multiple code generation engines named "${flags.engine}" found.`
          );
        }

        outputFiles = success.result as Record<string, string[]>;
      }
    }

    if (!outputFiles) {
      this.error(
        `No code generation engine named "${flags.engine}" found.`
      );
    }

    // Check if the list flag is set
    if (flags.list) {
      if (flags.format === 'json') {
        this.log(
          JSON.stringify(
            {
              fileNames: Object.keys(outputFiles),
              status: 'success'
            },
            null,
            2
          )
        );
      } else {
        for (const file of Object.keys(outputFiles))
          this.log(`> ${file}`);
      }

      return;
    }

    // handle preview flag
    if (flags.preview) {
      let contentToPreview = '';

      if (flags.file) {
        // Preview only the specified file
        if (outputFiles[flags.file]) {
          contentToPreview =
            flags.format === 'json'
              ? JSON.stringify(
                  {
                    file: {[flags.file]: outputFiles[flags.file]},
                    status: 'success'
                  },
                  null,
                  2
                )
              : outputFiles[flags.file].join('\n') + '\n';
        } else {
          this.error(
            `No file named "${flags.file}" found. Use --list to see the valid filenames.`
          );
        }
      } else {
        contentToPreview =
          flags.format === 'json'
            ? JSON.stringify(
                {files: outputFiles, status: 'success'},
                null,
                2
              )
            : Object.entries(outputFiles)
                .map(([fileName, fileContent]) =>
                  this.formatTextContent(fileName, fileContent)
                )
                .join('');
      }

      this.log(contentToPreview);
    } else {
      // put files on disk

      const outputDir = path.resolve(flags.output);

      if (flags.force) {
        await fs
          .mkdir(outputDir, {recursive: true})
          .catch((error) => {
            throw new Error(
              `Could not create output directory "${outputDir}".\n${error}`
            );
          });
      } else {
        // Check if the output directory exists when not using --force
        const outputDirExists = await fs
          .access(outputDir)
          .then(() => true)
          .catch(() => false);

        if (!outputDirExists) {
          this.error(
            `The output directory does not exist. Use the --force flag to create it automatically.`
          );
        }

        const filesToCheck = flags.file
          ? [flags.file]
          : Object.keys(outputFiles);
        const existingFiles: Array<string> = [];

        for (const outputFileName of filesToCheck) {
          const outputFile = path.resolve(outputDir, outputFileName);
          const fileExists = await fs
            .access(outputFile)
            .then(() => true)
            .catch(() => false);

          if (fileExists) {
            existingFiles.push(outputFileName);
          }
        }

        if (existingFiles.length > 0) {
          this.error(
            `The file(s) "${existingFiles.join(', ')}" already exist. Use the --force flag to overwrite.`
          );
        }
      }

      await this.writeGeneratedCodeToFiles(outputFiles, outputDir, {
        verbose: flags.verbose,
        file: flags.file,
        format: flags.format
      });
    }
  }

  async writeFileContent(
    outputFile: string,
    fileContent: string
  ): Promise<void> {
    await fs.writeFile(outputFile, fileContent).catch((error) => {
      throw new Error(
        `Could not write to "${outputFile}".\n${error}`
      );
    });
  }

  async writeGeneratedCodeToFiles(
    outputFiles: Record<string, string[]>,
    outputDir: string,
    flags: {
      verbose: boolean;
      file: string | undefined;
      format: string;
    }
  ) {
    if (flags.file) {
      // Handling single file output
      if (outputFiles[flags.file]) {
        const outputFile = path.resolve(outputDir, flags.file);
        const content = this.formatFileContent(
          flags.format,
          outputFiles[flags.file]
        );

        if (flags.verbose) {
          this.log('Writing output to:', outputFile);
        }

        await this.writeFileContent(outputFile, content);
      } else {
        this.error(`No file named "${flags.file}" found.`);
      }
    } else {
      for (const [outputFileName, outputLines] of Object.entries(
        outputFiles
      )) {
        const outputFile = path.resolve(outputDir, outputFileName);

        if (flags.verbose) {
          this.log('Writing output to', outputFile);
        }

        const formattedOutputLines = this.formatFileContent(
          flags.format,
          outputLines
        );

        // Write output to file
        await this.writeFileContent(outputFile, formattedOutputLines);
      }
    }
  }
}
