// Copyright (C) 2023 WYATT GROUP
// Please see the AUTHORS file for details.
//
// 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, either version 3 of the License, or
// any later version.
//
// 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 'dart:io';

import 'package:brick_generator/core/logger.dart';
import 'package:brick_generator/core/shell.dart';
import 'package:brick_generator/models/brick_config.dart';
import 'package:brick_generator/models/brick_variable_string.dart';
import 'package:brick_generator/models/ignore_list.dart';
import 'package:path/path.dart';

abstract class FileSystem {
  static void writeFile(String path, Object? content, {bool binary = false}) {
    final file = File(join(path))..createSync(recursive: true);

    if (content == null) {
      // Just create an empty file
      return;
    }

    try {
      if (binary) {
        file.writeAsBytesSync(content as List<int>);
      } else {
        file.writeAsStringSync(content as String);
      }
    } catch (e) {
      Logger.error(e);
    }
  }

  static Future<void> copyFolder(
    String from,
    String to, {
    IgnoreList? ignoreList,
  }) async {
    if (!FileSystemEntity.isDirectorySync(from)) {
      throw ArgumentError('Path must be a directory', 'from');
    }

    Future<void> cp(FileSystemEntity entity) async {
      final absolutPath = entity.path;
      final relativePath = absolutPath.replaceFirst(from, '');
      final newPath = ((to + relativePath).split('/')..removeLast()).join('/');

      if (ignoreList != null && ignoreList.contains(from, absolutPath)) {
        Logger.debug('Ignoring $absolutPath');
      } else {
        Logger.debug('cp -Rf $absolutPath $newPath');
        await Shell.cp(absolutPath, newPath);
      }
    }

    // Copy folders
    await Future.wait<void>(
      Directory(from)
          .listSync(recursive: true)
          .whereType<Directory>()
          .map((directory) async {
        await cp(directory);
      }),
    );

    // Copy root files
    await Future.wait<void>(
      Directory(from).listSync().whereType<File>().map((file) async {
        await cp(file);
      }),
    );
  }

  static void makeDirectory(String path) {
    final dir = Directory(path);
    if (dir.existsSync()) {
      dir.deleteSync(recursive: true);
    }
    dir.createSync(recursive: true);
  }

  /// Join and verify if the joined path exists.
  static String joinExists(
    String part1, [
    String? part2,
    String? part3,
    String? part4,
    String? part5,
    String? part6,
    String? part7,
    String? part8,
  ]) {
    final path = join(part1, part2, part3, part4, part5, part6, part7, part8);

    if (FileSystemEntity.isDirectorySync(path)) {
      Logger.debug('$path is a directory, and it exists.');
      return path;
    }

    if (FileSystemEntity.isFileSync(path)) {
      Logger.debug('$path is a file, and it exists.');
      return path;
    }

    throw ArgumentError('Joined path is not existing.');
  }

  /// Join and verify if the joined path exists. If not, create it.
  static String joinCreate(
    String part1, [
    String? part2,
    String? part3,
    String? part4,
    String? part5,
    String? part6,
    String? part7,
    String? part8,
  ]) {
    final path = join(part1, part2, part3, part4, part5, part6, part7, part8);

    if (FileSystemEntity.isDirectorySync(path)) {
      Logger.debug('$path is a directory, and it exists.');
      return path;
    }

    if (FileSystemEntity.isFileSync(path)) {
      Logger.debug('$path is a file, and it exists.');
      return path;
    }

    makeDirectory(path);
    Logger.debug('$path directory was created.');
    return path;
  }

  /// Join and verify if the joined path exists. If yes, recreate it.
  static String joinRecreate(
    String part1, [
    String? part2,
    String? part3,
    String? part4,
    String? part5,
    String? part6,
    String? part7,
    String? part8,
  ]) {
    final path = join(part1, part2, part3, part4, part5, part6, part7, part8);

    if (FileSystemEntity.isDirectorySync(path)) {
      makeDirectory(path);
      Logger.debug('$path directory was recreated.');
      return path;
    }

    if (FileSystemEntity.isFileSync(path)) {
      Logger.debug('$path is a file ! No recreate it!');
      return path;
    }

    makeDirectory(path);
    Logger.debug('$path directory was created.');
    return path;
  }

  /// Join and verify if the joined path exists. If yes, touch it. If not
  /// create an empty file.
  static String joinTouch(
    String part1, [
    String? part2,
    String? part3,
    String? part4,
    String? part5,
    String? part6,
    String? part7,
    String? part8,
  ]) {
    final path = join(part1, part2, part3, part4, part5, part6, part7, part8);

    if (FileSystemEntity.isDirectorySync(path)) {
      Logger.debug('$path is a directory ! No recreate it!');
      return path;
    }

    if (FileSystemEntity.isFileSync(path)) {
      writeFile(path, null);
      Logger.debug('$path files was touched!');
      return path;
    }

    writeFile(path, null);
    Logger.debug('$path file was created.');
    return path;
  }

  /// Transforms compilable variables:
  /// For each String `vars` in config, search for compilable in code, then
  /// convert it to his mustache format.
  ///
  /// Example:
  /// ```yaml
  /// bundle_id:
  ///   compilable: io.wyattapp.new
  ///   type: string
  ///   description: The bundle id used in Android and iOS
  ///   default: io.wyattapp.new
  ///   prompt: "What is the bundle id?"
  /// ```
  ///
  /// Search for `io.wyattapp.new` then converts
  /// it to `{{#dotCase}}{{bundle_id}}{{/dotCase}}`
  static void convertCompilableVariablesInFolder(
    BrickConfig config,
    String targetPath,
  ) {
    if (!FileSystemEntity.isDirectorySync(targetPath)) {
      throw ArgumentError('Target must be a directory', 'targetPath');
    }

    Directory(targetPath)
        .listSync(recursive: true)
        .whereType<File>()
        .forEach((file) {
      String? contents;

      try {
        contents = file.readAsStringSync();
      } catch (e) {
        // Ignore decode error
        return;
      }

      config.variables.whereType<BrickVariableString>().forEach((variable) {
        // Replace all string variables
        for (final syntax in variable.formats) {
          final toReplace = variable.syntaxes?[syntax.mapKey];
          if (toReplace != null && (contents?.contains(toReplace) ?? false)) {
            contents = contents!.replaceAll(
              toReplace,
              '{{#${syntax.id}}}{{${variable.name}}}{{/${syntax.id}}}',
            );
            Logger.debug(
              'Variable `${variable.name}` replaced '
              'in ${file.path} by $toReplace',
            );
          }
        }
      });

      file.writeAsStringSync(contents!);
    });
  }

  static void createGitKeepInEmptyFolders(String targetPath) {
    if (!FileSystemEntity.isDirectorySync(targetPath)) {
      throw ArgumentError('Target must be a directory', 'targetPath');
    }

    Directory(targetPath)
        .listSync(recursive: true)
        .whereType<Directory>()
        .forEach((directory) {
      if (directory.existsSync() &&
          directory.listSync(recursive: true).isEmpty) {
        joinTouch(directory.path, '.gitkeep');
        Logger.debug('${directory.path} (empty) marked as to keep.');
      }
    });
  }

  /// Removes empty folder dynamically:
  ///
  /// For example, for this structure:
  /// ```
  /// ./brick/__brick__/folder/empty1/
  /// ./brick/__brick__/folder/empty2/
  /// ./brick/__brick__/dummy.txt
  /// ```
  /// it will delete `empty1` and `empty2`...then it will delete `folder`,
  /// because by deleting his subfolders he's now empty too.
  ///
  static void removeEmptyFolders(String targetPath) {
    if (!FileSystemEntity.isDirectorySync(targetPath)) {
      throw ArgumentError('Target must be a directory', 'targetPath');
    }

    bool containsEmptyFoldersFunction() => Directory(targetPath)
        .listSync(recursive: true)
        .whereType<Directory>()
        .any(
          (directory) =>
              directory.existsSync() &&
              directory.listSync(recursive: true).isEmpty,
        );

    while (containsEmptyFoldersFunction()) {
      Directory(targetPath)
          .listSync(recursive: true)
          .whereType<Directory>()
          .forEach((directory) {
        if (directory.existsSync() &&
            directory.listSync(recursive: true).isEmpty) {
          directory.deleteSync(recursive: true);
          Logger.debug('${directory.path} (empty) deleted.');
        }
      });
    }
  }

  /// Transforms compilable file/folder names:
  /// For each String `vars` in config, search for compilable in code, then
  /// convert it to his mustache format.
  ///
  /// Example:
  /// ```yaml
  /// project_name:
  ///   compilable: wyatt_app_template
  ///   type: string
  ///   description: The project name
  ///   default: wyatt_app
  ///   prompt: "What is the project name?"
  /// ```
  ///
  /// Search for `wyatt_app_template` files and folders then converts
  /// it to `{{project_name.snakeCase()}}`
  static void renameCompilableFilesInFolder(
    BrickConfig config,
    String targetPath,
  ) {
    if (!FileSystemEntity.isDirectorySync(targetPath)) {
      throw ArgumentError('Target must be a directory', 'targetPath');
    }

    Directory(targetPath)
        .listSync(recursive: true)
        .whereType<File>()
        .forEach((file) {
      config.variables.whereType<BrickVariableString>().forEach((variable) {
        // Replace all string variables
        for (final syntax in variable.formats) {
          final toReplace = variable.syntaxes?[syntax.mapKey];
          if (toReplace != null && file.path.contains(toReplace)) {
            final newPath = file.path.replaceAll(
              toReplace,
              '{{${variable.name}.${syntax.id}()}}',
            );

            File(newPath).createSync(recursive: true);
            file.renameSync(newPath);
            Logger.debug(
              '${file.path} renamed with variable '
              '`${variable.name}` using $syntax',
            );
          }
        }
      });
    });
  }

  static Future<void> copyHooks(
    BrickConfig config,
    String source,
    String targetPath,
  ) async {
    if (!config.brickgenConfig.hooks) {
      Logger.debug('Ignore hooks');
      return;
    }

    if (!FileSystemEntity.isDirectorySync(source)) {
      throw ArgumentError('Source must be a directory', 'source');
    }

    if (!FileSystemEntity.isDirectorySync(targetPath)) {
      throw ArgumentError('Target must be a directory', 'targetPath');
    }

    await copyFolder(source, targetPath);
    Logger.info('Hooks copied.');
  }

  /// Apply boolean file system: create or not folders/files depending of
  /// a boolean variable.
  ///
  /// Example
  /// ```yaml
  /// boolean_file_system:
  /// flutter:
  ///   folders:
  ///     on_true:
  ///       - android
  ///       - ios
  ///       - assets
  ///     on_false:
  ///       - bin
  ///   files:
  ///     on_true:
  ///       - l10n.yaml
  ///       - trapeze.yaml
  /// ```
  ///
  /// Creates some folders and files depending of `flutter` variable.
  static void applyBooleanFileSystem(
    BrickConfig config,
    String targetPath,
  ) {
    for (final variable in config.brickgenConfig.booleanFileSystem) {
      final boolean = variable.booleanName;

      String rename(
        String boolean,
        FileSystemEntity entity, {
        bool? onValue,
      }) {
        final entityPath = entity.path;
        final entityPathSplitted =
            (entityPath.split('/')..removeWhere((element) => element.isEmpty));
        final entityRenamed =
            '{{${(onValue ?? true) ? "#" : "^"}$boolean}}${entityPathSplitted.last}{{/$boolean}}';
        final entityNewPath =
            ((entityPathSplitted..removeLast())..add(entityRenamed)).join('/');

        Logger.debug('Apply boolean file system on $entityPath');

        return entityNewPath;
      }

      // Rename folders
      for (final folder in variable.folders.onTrueNames) {
        final directory = Directory(join(targetPath, folder));
        final newPath = rename(
          boolean,
          directory,
          onValue: true,
        );

        Directory(newPath).createSync(recursive: true);
        directory.renameSync(newPath);
      }

      for (final folder in variable.folders.onFalseNames) {
        final directory = Directory(join(targetPath, folder));
        final newPath = rename(
          boolean,
          directory,
          onValue: false,
        );

        Directory(newPath).createSync(recursive: true);
        directory.renameSync(newPath);
      }

      // Rename files
      for (final path in variable.files.onTrueNames) {
        final file = File(join(targetPath, path));
        final newPath = rename(
          boolean,
          file,
          onValue: true,
        );

        File(newPath).createSync(recursive: true);
        file.renameSync(newPath);
      }

      for (final path in variable.files.onFalseNames) {
        final file = File(join(targetPath, path));
        final newPath = rename(
          boolean,
          file,
          onValue: false,
        );

        File(newPath).createSync(recursive: true);
        file.renameSync(newPath);
      }
    }
  }
}
