# encoding: UTF-8

module Tetra
  # encapsulates a Tetra project directory
  class Project
    include Logging

    # path of the project template files
    TEMPLATE_PATH = File.join(File.dirname(__FILE__), "..", "template")

    attr_reader :full_path

    def initialize(path)
      @full_path = Tetra::Project.find_project_dir(File.expand_path(path))
      @git = Tetra::Git.new(@full_path)
    end

    def name
      File.basename(@full_path)
    end

    def version
      @git.latest_id("tetra: dry-run-finished")
    end

    def packages_dir
      "packages"
    end

    # finds the project directory up in the tree, like git does
    def self.find_project_dir(starting_dir)
      result = starting_dir
      while project?(result) == false && result != "/"
        result = File.expand_path("..", result)
      end

      fail NoProjectDirectoryError, starting_dir if result == "/"

      result
    end

    # returns true if the specified directory is a valid tetra project
    def self.project?(dir)
      File.directory?(File.join(dir, "src")) &&
        File.directory?(File.join(dir, "kit")) &&
        File.directory?(File.join(dir, ".git"))
    end

    # inits a new project directory structure
    def self.init(dir, include_bundled_software = true)
      Dir.chdir(dir) do
        Tetra::Git.new(".").init

        FileUtils.mkdir_p("src")
        FileUtils.mkdir_p("kit")

        # populate the project with templates and commit it
        project = Project.new(".")

        project.template_files(include_bundled_software).each do |source, destination|
          FileUtils.cp_r(File.join(TEMPLATE_PATH, source), destination)
        end

        project.commit_whole_directory(".", "Template files added")
      end
    end

    # returns a hash that maps filenames that should be copied from TEMPLATE_PATH
    # to the value directory
    def template_files(include_bundled_software)
      result = {
        "kit" => ".",
        "packages" => ".",
        "src" => ".",
        "gitignore" => ".gitignore"
      }

      if include_bundled_software
        Dir.chdir(TEMPLATE_PATH) do
          Dir.glob(File.join("bundled", "*")).each do |file|
            result[file] = "kit"
          end
        end
      end

      result
    end

    # checks whether there were edits to src/
    # since last mark
    def src_patched?
      from_directory do
        latest_id = @git.latest_id("tetra: sources-")
        if latest_id
          @git.changed_files("src", latest_id).any?
        else
          false
        end
      end
    end

    # starts a dry running phase: files added to kit/ will be added
    # to the kit package, src/ will be reset at the current state
    # when finished
    def dry_run
      current_directory = Pathname.new(Dir.pwd).relative_path_from(Pathname.new(@full_path))

      commit_whole_directory(".", "Dry-run started\n", "tetra: dry-run-started: #{current_directory}")
    end

    # returns true iff we are currently dry-running
    def dry_running?
      latest_comment = @git.latest_comment("tetra: dry-run-")
      !latest_comment.nil? && !(latest_comment =~ /tetra: dry-run-finished/)
    end

    # ends a dry-run assuming a successful build
    # reverts sources and updates output file lists
    def finish
      # keep track of changed files
      start_id = @git.latest_id("tetra: dry-run-started")
      changed_files = @git.changed_files("src", start_id)

      # revert to pre-dry-run status
      @git.revert_whole_directory("src", start_id)

      # prepare commit comments
      comments = ["Dry run finished\n", "tetra: dry-run-finished"]
      comments += changed_files.map { |f| "tetra: file-changed: #{f}" }

      # if this is the first dry-run, mark sources as tarball
      if @git.latest_id("tetra: dry-run-finished").nil?
        comments << "tetra: sources-tarball"
      end

      # commit end of dry run
      commit_whole_directory(".", *comments)
    end

    # ends a dry-run assuming the build went wrong
    # reverts the whole project directory
    def abort
      @git.revert_whole_directory(".", @git.latest_id("tetra: dry-run-started"))
      @git.undo_last_commit
    end

    # commits all files in the directory
    def commit_whole_directory(directory, *comments)
      # rename all .gitignore files that might have slipped in
      from_directory("src") do
        Find.find(".") do |file|
          next unless file =~ /\.gitignore$/

          FileUtils.mv(file, "#{file}_disabled_by_tetra")
        end
      end

      @git.commit_whole_directory(directory, comments.join("\n"))
    end

    # commits files in the src/ dir as a patch or tarball update
    def commit_sources(message, new_tarball = false)
      from_directory do
        comments = ["#{message}\n"]
        comments << "tetra: sources-tarball" if new_tarball
        commit_whole_directory("src", comments)
      end
    end

    # replaces content in path with new_content, commits using
    # comment and 3-way merges new and old content with the previous
    # version of file of the same kind, if it exists.
    # returns the number of conflicts
    def merge_new_content(new_content, path, comment, kind)
      from_directory do
        log.debug "merging new content to #{path} of kind #{kind}"
        already_existing = File.exist?(path)

        generated_comment = "tetra: generated-#{kind}"
        whole_comment = [comment, generated_comment].join("\n\n")

        if already_existing
          unless @git.latest_id(generated_comment)
            log.debug "committing new file"
            @git.commit_file(path, whole_comment)
          end
          log.debug "moving #{path} to #{path}.tetra_user_edited"
          File.rename(path, "#{path}.tetra_user_edited")
        end

        previous_id = @git.latest_id(generated_comment)

        File.open(path, "w") { |io| io.write(new_content) }
        log.debug "committing new content: #{comment}"
        @git.commit_file(path, whole_comment)

        if already_existing
          # 3-way merge
          conflict_count = @git.merge_with_id(path, "#{path}.tetra_user_edited", previous_id)
          File.delete("#{path}.tetra_user_edited")

          @git.commit_file(path, "User changes merged back") if conflict_count == 0

          return conflict_count
        end
        return 0
      end
    end

    # runs a block from the project directory or a subdirectory
    def from_directory(subdirectory = "")
      Dir.chdir(File.join(@full_path, subdirectory)) do
        yield
      end
    end

    # returns the latest dry run start directory
    def latest_dry_run_directory
      @git.latest_comment("tetra: dry-run-started")[/tetra: dry-run-started: (.*)$/, 1]
    end

    # returns a list of files produced during the last dry-run
    def produced_files
      @git.latest_comment("tetra: dry-run-finished")
        .split("\n")
        .map { |line| line[/^tetra: file-changed: src\/(.+)$/, 1] }
        .compact
        .sort
    end

    # archives a tarball of src/ in packages/
    # the latest commit marked as tarball is taken as the version
    def archive_sources
      from_directory do
        id = @git.latest_id("tetra: sources-tarball")
        destination_path = File.join(full_path, packages_dir, name, "#{name}.tar.xz")
        @git.archive("src", id, destination_path)
      end
    end

    # archives a tarball of kit/ in packages/
    # the latest commit marked as dry-run-finished is taken as the version
    def archive_kit
      from_directory do
        id = @git.latest_id("tetra: dry-run-finished")
        destination_path = File.join(full_path, packages_dir, "#{name}-kit", "#{name}-kit.tar.xz")
        @git.archive("kit", id, destination_path)
      end
    end

    # generates patches of src/ in packages/
    # the latest commit marked as tarball is taken as the base version,
    # other commits are assumed to be patches on top
    # returns filenames
    def write_source_patches
      from_directory do
        id = @git.latest_id("tetra: sources-tarball")
        destination_path = File.join(full_path, packages_dir, name)
        @git.format_patch("src", id, destination_path)
      end
    end

    # moves any .jar from src/ to kit/ and links it back
    def purge_jars
      from_directory do
        result = []
        Find.find("src") do |file|
          next unless file =~ /.jar$/ && !File.symlink?(file)

          new_location = File.join("kit", "jars", Pathname.new(file).split[1])
          FileUtils.mv(file, new_location)

          link_target = Pathname.new(new_location)
                        .relative_path_from(Pathname.new(file).split.first)
                        .to_s

          File.symlink(link_target, file)
          result << [file, new_location]
        end

        result
      end
    end
  end

  # current directory is not a tetra project
  class NoProjectDirectoryError < StandardError
    attr_reader :directory

    def initialize(directory)
      @directory = directory
    end
  end
end