require 'kameleon/utils'
require 'kameleon/step'

module Kameleon

  class Recipe
    attr_accessor :path
    attr_accessor :name
    attr_accessor :global
    attr_accessor :sections
    attr_accessor :aliases
    attr_accessor :aliases_path
    attr_accessor :checkpoint
    attr_accessor :checkpoint_path
    attr_accessor :metainfo
    attr_accessor :files
    attr_accessor :extended_recipe_file
    attr_accessor :base_recipes_files
    attr_accessor :data_files
    attr_accessor :env_files
    attr_accessor :cli_global

    def initialize(path, kwargs = {})
      @path = Pathname.new(File.expand_path(path))
      if not @path.exist? and @path.extname != ".yaml"
        @path = Pathname.new(File.expand_path(path + ".yaml"))
      end
      @name = (@path.basename ".yaml").to_s
      @recipe_content = File.open(@path, 'r') { |f| f.read }
      @sections = {
        "bootstrap" => Section.new("bootstrap"),
        "setup" => Section.new("setup"),
        "export" => Section.new("export"),
      }
      @cli_global = Kameleon.env.global.clone
      @cli_global.each do |k,v|
        Kameleon.ui.warn("CLI Global variable override: #{k} => #{v}")
      end

      @global = {
        "kameleon_recipe_name" => @name,
        "kameleon_recipe_dir" => File.dirname(@path),
        "kameleon_cwd" => File.join(Kameleon.env.build_path, @name),
        "in_context" => {"cmd" => "/bin/bash", "proxy_cache" => "127.0.0.1"},
        "out_context" => {"cmd" => "/bin/bash", "proxy_cache" => "127.0.0.1"},
        "proxy_local" => "",
        "proxy_out" => "",
        "proxy_in" => "",
        "checkpointing_enabled" => "false",
        "persistent_cache" => "false",
      }
      @aliases = {}
      @checkpoint = nil
      @step_files = []
      Kameleon.ui.verbose("Initialize new recipe (#{path})")
      @base_recipes_files = [@path]
      @data_files = []
      @env_files = []
      @steps_dirs = []
      load! :strict => false
    end

    def update_steps_dirs()
      # Where we can find steps
      @steps_dirs = @base_recipes_files.map do |recipe_path|
        get_steps_dirs(recipe_path)
      end.flatten!
    end

    def get_steps_dirs(recipe_path)
      relative_path = recipe_path.to_s.gsub(Kameleon.env.root_dir.to_s + '/', '')
      if relative_path.eql? recipe_path.to_s
        subdirs = [recipe_path.dirname]
      else
        last_dir = Kameleon.env.root_dir
        subdirs = [last_dir]
        relative_path.split("/")[0...-1].each do |p|
          subdir = last_dir.join(p)
          subdirs.push(subdir)
          last_dir = subdir
        end
      end
      steps_dirs = []
      subdirs.reverse_each do |p|
        steps_dirs.push(File.expand_path(File.join(p.to_s, 'steps')))
        steps_dirs.push(File.expand_path(File.join(p.to_s, '.steps')))
      end
      steps_dirs.select! { |x| File.exist? x }
    end

    def load!(kwargs = {})
      # Find recipe path
      Kameleon.ui.verbose("Loading #{@path}")
      fail RecipeError, "Could not find this following recipe: #{@path}" \
         unless File.file? @path
      yaml_recipe = YAML.unsafe_load_file @path
      unless yaml_recipe.kind_of? Hash
        fail RecipeError, "Invalid yaml: #{@path}"
      end

      update_steps_dirs()

      extended_recipe_name = yaml_recipe.fetch("extend", "")
      unless extended_recipe_name.nil?
        extended_recipe_name << ".yaml" unless extended_recipe_name.end_with? ".yaml"
        @extended_recipe_file = Pathname.new(File.expand_path(File.join(File.dirname(path), extended_recipe_name)))
      end

      # Load extended recipe variables
      yaml_recipe = load_base_recipe(yaml_recipe, @path)
      yaml_recipe.delete("extend")

      # Where we can find steps
      @steps_dirs = @base_recipes_files.map do |recipe_path|
        get_steps_dirs(recipe_path)
      end.flatten!
      @steps_dirs.uniq!

      # Set default value for in_ctx and out_ctx options
      %w(out_context in_context).each do |context_name|
        unless yaml_recipe.keys.include? "global"
          yaml_recipe["global"] = {}
        end
        unless yaml_recipe["global"].keys.include? context_name
          yaml_recipe["global"][context_name] = {}
        end
        @global[context_name].merge!(yaml_recipe["global"][context_name])
        yaml_recipe["global"][context_name] = @global[context_name]
        unless yaml_recipe["global"][context_name].keys.include? "interactive_cmd"
          yaml_recipe["global"][context_name]["interactive_cmd"] = yaml_recipe["global"][context_name]['cmd']
        end
      end
      # Load Global variables
      @global.merge!(yaml_recipe.fetch("global", {}))
      # merge cli variable with recursive variable overload
      @global = Utils.overload_merge(@global, @cli_global)

      # Resolve dynamically-defined variables !!
      resolved_global = Utils.resolve_vars(@global.to_yaml, @path, @global, self, kwargs)
      resolved_global = @global.merge YAML.unsafe_load(resolved_global)
      Kameleon.ui.debug("Resolved_global: #{resolved_global}")
      # Loads aliases
      load_aliases(yaml_recipe)
      # Load env files
      load_env_files(yaml_recipe)
      # Loads checkpoint configuration
      load_checkpoint_config(yaml_recipe)
      include_steps = resolved_global['include_steps']
      include_steps ||= []
      include_steps.push ''
      include_steps.flatten!
      include_steps.compact!
      Kameleon.ui.debug("include steps: #{include_steps}")
      @sections.values.each do |section|
        dir_to_search = @steps_dirs.map do |steps_dir|
          include_steps.map do |path|
            [File.join(steps_dir, section.name, path),
              File.join(steps_dir, path)]
          end
        end.flatten.select { |x| File.exist? x }
        Kameleon.ui.debug("Directory to search for steps:  #{dir_to_search}")

        if yaml_recipe.key? section.name
          yaml_section = yaml_recipe.fetch(section.name)
          next unless yaml_section.kind_of? Array
          yaml_section.each do |raw_macrostep|
            embedded_step = false
            # Get macrostep name and arguments if available
            if raw_macrostep.kind_of? String
              name = Utils.resolve_vars(raw_macrostep, @path, @global, self, kwargs)
              args = nil
            elsif raw_macrostep.kind_of? Hash
              name = Utils.resolve_vars(raw_macrostep.keys[0], @path, @global, self, kwargs)
              args = raw_macrostep.values[0]
            else
              fail RecipeError, "Malformed yaml recipe in section: "\
                                "#{section.name}"
            end
            # Detect if step is embedded
            if not args.nil?
              args.each do |arg|
                if arg.kind_of? Hash
                  if arg[arg.keys[0]].kind_of? Array
                    embedded_step = true
                  end
                end
              end
            end
            if embedded_step
              Kameleon.ui.verbose("Loading embedded macrostep #{name}")
              macrostep = load_macrostep(nil, name, args, kwargs)
              section.macrosteps.push(macrostep)
              next
            end
            # Load macrostep yaml
            loaded = false
            dir_to_search.each do |dir|
              macrostep_path = Pathname.new(File.join(dir, name + '.yaml'))
              if File.file?(macrostep_path)
                Kameleon.ui.verbose("Loading macrostep #{macrostep_path}")
                macrostep = load_macrostep(macrostep_path, name, args, kwargs)
                section.macrosteps.push(macrostep)
                @step_files.push(macrostep_path)
                Kameleon.ui.verbose("Macrostep '#{name}' found in this path: " \
                                  "#{macrostep_path}")
                loaded = true
                break
              else
                Kameleon.ui.verbose("Macrostep '#{name}' not found in this path: " \
                              "#{macrostep_path}")
              end
            end
            fail RecipeError, "Step #{name} not found" unless loaded
          end
        end
      end
      Kameleon.ui.verbose("Loading recipe metadata")
      @metainfo = {
        "description" => Utils.extract_meta_var("description", @recipe_content)
      }
    end

    def load_base_recipe(yaml_recipe, path)

      base_recipe_name = yaml_recipe.fetch("extend", "")
      return yaml_recipe if base_recipe_name.empty?

      # resolve variable in extends to permit backend selection
      base_recipe_name = Utils.resolve_simple_vars_once(
          base_recipe_name, Utils.overload_merge(load_global(yaml_recipe, path), @cli_global))

      ## check that the recipe has not already been loaded
      base_recipe_name << ".yaml" unless base_recipe_name.end_with? ".yaml"
      base_recipe_path = File.join(File.dirname(path), base_recipe_name)

      ## check that the recipe has not already been loaded
      return yaml_recipe if @base_recipes_files.include? base_recipe_path

      @base_recipes_files.push(Pathname.new(File.expand_path(base_recipe_path)))
      update_steps_dirs()

      base_recipe_path << ".yaml" unless base_recipe_path.end_with? ".yaml"
      fail RecipeError, "Could not find this following recipe: #{@recipe_path}" \
         unless File.file? path
      base_yaml_recipe = YAML.unsafe_load_file base_recipe_path
      unless yaml_recipe.kind_of? Hash
        fail RecipeError, "Invalid yaml: #{base_yaml_recipe}"
      end
      base_yaml_recipe.keys.each do |key|
        if ["export", "bootstrap", "setup"].include? key
          base_yaml_recipe.delete(key) unless yaml_recipe.keys.include? key
        end
      end
      yaml_recipe.keys.each do |key|
        if ["aliases", "checkpoint", "env"].include? key
          base_yaml_recipe[key] = yaml_recipe[key]
        elsif ["export", "bootstrap", "setup"].include? key
          base_section = base_yaml_recipe.fetch(key, [])
          base_section = [] if base_section.nil?
          recipe_section = yaml_recipe[key]
          recipe_section = [] if recipe_section.nil?
          index_base_steps = recipe_section.index("@base")
          unless index_base_steps.nil?
            recipe_section[index_base_steps] = base_section
            recipe_section.flatten!
          end
          base_yaml_recipe[key] = recipe_section
        elsif ["global"].include? key
          base_section = load_global(base_yaml_recipe, base_recipe_path)
          recipe_section = load_global(yaml_recipe, path)
          # manage recursive variable overload
          base_yaml_recipe[key] = Utils.overload_merge(base_section, recipe_section)
        end
      end
      return load_base_recipe(base_yaml_recipe, base_recipe_path)
    end

    def load_global(yaml_recipe, recipe_path)
      global = {}
      if yaml_recipe.keys.include? "global"
        global_loaded = yaml_recipe.fetch("global", {})
        global_loaded = {} if global_loaded.nil?
        if global_loaded.kind_of? Hash
          global_loaded.each do |key, value|
            if key.eql? "include"
              global_to_include = load_include_global(value, recipe_path)
              global.merge!(global_to_include)
            else
              global[key] = value
            end
          end
        end
      end
      return global
    end

    def load_include_global(yaml_include, recipe_path)
      def load_global_file(global_file, recipe_path)
        def try_to_load(absolute_path)
          if File.file?(absolute_path)
            global_to_include = YAML.unsafe_load_file(absolute_path)
            if global_to_include.kind_of? Hash
              @step_files.push(absolute_path)
              return global_to_include
            else
              fail RecipeError, "Global should be a Hash. (check #{absolute_path})"
            end
          end
        end
        ## check that the recipe has not already been loaded
        global_file << ".yaml" unless global_file.end_with? ".yaml"

        dir_search = @steps_dirs.map do |steps_dir|
          File.join(steps_dir, "global")
        end.flatten
        dir_search.unshift(File.join(File.dirname(recipe_path)))
        # try relative/absolute path
        if Pathname.new(global_file).absolute?
          global_to_include = try_to_load(global_file)
          unless global_to_include.nil?
            return global_to_include
          else
            fail RecipeError, "File '#{global_file}' not found"
          end
        else
          dir_search.each do |dir_path|
            absolute_path = Pathname.new(File.join(dir_path, global_file))
            global_to_include = try_to_load(absolute_path)
            unless global_to_include.nil?
              return global_to_include
            end
          end
        end
        rel_dir_search = dir_search.map do |steps_dir|
          Pathname.new(steps_dir).relative_path_from(Pathname(Dir.pwd)).to_s
        end.flatten
        fail RecipeError, "File '#{global_file}' not found here #{rel_dir_search}"
      end

      global_hash = {}
      if yaml_include.kind_of? String
        list_files = [yaml_include]
      elsif yaml_include.kind_of? Array
        list_files = []
        yaml_include.each do |value|
          if value.kind_of? String
            list_files.push(value)
          end
        end
      else
        return global_hash
      end
      list_files.each do |includes_file|
        filename = includes_file
        if includes_file.start_with?("-")
          filename = includes_file[1..-1]
        end
        begin
          new_global = load_global_file(filename, recipe_path)
          global_hash.merge!(new_global)
        rescue
          unless includes_file.start_with?("-")
            raise
          end
        end
      end
      return global_hash
    end

    def load_aliases(yaml_recipe)
      def load_aliases_file(aliases_file)
        dir_search = @steps_dirs.map do |steps_dir|
          File.join(steps_dir, "aliases")
        end.flatten
        dir_search.each do |dir_path|
          path = Pathname.new(File.join(dir_path, aliases_file))
          if File.file?(path)
            Kameleon.ui.verbose("Loading aliases #{path}")
            @aliases.merge!(YAML.unsafe_load_file(path))
            @step_files.push(path)
            return path
          end
        end
        fail RecipeError, "Aliases file for recipe '#{@path}' does not exists"
      end
      if yaml_recipe.keys.include? "aliases"
        aliases = yaml_recipe.fetch("aliases")
        if aliases.kind_of? Hash
          @aliases = aliases
        elsif aliases.kind_of? String
          load_aliases_file(aliases)
        elsif aliases.kind_of? Array
          aliases.each do |aliases_file|
            load_aliases_file(aliases_file)
          end
        end
      end
    end

    def load_env_files(yaml_recipe)
      def add_env_file(env_file)
        dir_search = @steps_dirs.map do |steps_dir|
          File.join(steps_dir, "env")
        end.flatten
        dir_search.each do |dir_path|
          path = Pathname.new(File.join(dir_path, env_file))
          if File.file?(path)
            Kameleon.ui.verbose("Adding env file #{path}")
            @env_files.push(path)
            return path
          end
        end
        fail RecipeError, "The env file script '#{env_file}' does not exists "\
                          "in any of these directories: #{dir_search}"
      end
      if yaml_recipe.keys.include? "env"
        env_content = yaml_recipe.fetch("env")
        if env_content.kind_of? String
          add_env_file(env_content)
        elsif env_content.kind_of? Array
          env_content.each do |env_file|
            add_env_file(env_file)
          end
        end
      end
    end

    def load_checkpoint_config(yaml_recipe)
      if yaml_recipe.keys.include? "checkpoint"
        checkpoint = yaml_recipe.fetch("checkpoint")
        if checkpoint.kind_of? Hash
          @checkpoint = checkpoint
          @checkpoint["path"] = @path
        elsif checkpoint.kind_of? String
          dir_search = @steps_dirs.map do |steps_dir|
              File.join(steps_dir, "checkpoints")
          end.flatten
          dir_search.each do |dir_path|
            path = Pathname.new(File.join(dir_path, checkpoint))
            if File.file?(path)
              Kameleon.ui.verbose("Loading checkpoint configuration #{path}")
              @checkpoint = YAML.unsafe_load_file(path)
              @checkpoint["path"] = path.to_s
              @step_files.push(path)
              break
            end
          end
          fail RecipeError, "Checkpoint configuraiton file '#{checkpoint}' " \
                            "does not exists" if @checkpoint.nil?
        end
        (@checkpoint.keys - ["path"]).each do |key|
          @checkpoint[key].map! do |cmd|
            Kameleon::Command.new(cmd, "checkpoint")
          end
        end
      end
    end

    def load_macrostep(step_path, name, args, kwargs)
      if step_path.nil?
        macrostep_yaml = args
        step_path = @path
      else
        macrostep_yaml = YAML.unsafe_load_file(step_path)
        # Basic macrostep syntax check
        if not macrostep_yaml.kind_of? Array
          fail RecipeError, "The macrostep #{step_path} is not valid "
                            "(should be a list of microsteps)"
        end
      end
      local_variables = {}
      loaded_microsteps = []
      # Load default local variables
      macrostep_yaml.each do |yaml_microstep|
        key = yaml_microstep.keys[0]
        value = yaml_microstep[key]
        # Set new variable if not defined yet
        if value.kind_of? Array
          loaded_microsteps.push Microstep.new(yaml_microstep)
        else
          local_variables[key] = @global.fetch(key, value)
        end
      end
      unless step_path.nil?
        selected_microsteps = []
        if args
          args.each do |entry|
            if entry.kind_of? Hash
              # resolve variable before using it
              entry.each do |key, value|
                local_variables[key] = value
              end
            elsif entry.kind_of? String
              selected_microsteps.push entry
            end
          end
        end
        unless selected_microsteps.empty?
          # Some steps are selected so remove the others
          # WARN: Allow the user to define this list not in the original order
          strip_microsteps = []
          selected_microsteps.each do |microstep_name|
            macrostep = find_microstep(microstep_name, loaded_microsteps)
            if macrostep.nil?
              fail RecipeError, "Can't find microstep '#{microstep_name}' "\
                                "in macrostep file '#{step_path}'"
            else
              strip_microsteps.push(macrostep)
            end
          end
          loaded_microsteps = strip_microsteps
        end
      end
      return Macrostep.new(name, loaded_microsteps, local_variables, step_path)
    end

    def find_microstep(microstep_name, loaded_microsteps)
      Kameleon.ui.verbose("Looking for microstep #{microstep_name}")
      loaded_microsteps.each do |microstep|
        if microstep_name.eql? microstep.name
          return microstep
        end
      end
      return nil
    end

    def resolve_data_path(partial_path, step_path)
      Kameleon.ui.verbose("Looking for data '#{partial_path}'")
      dir_search = @steps_dirs.map do |steps_dir|
          File.join(steps_dir, "data")
      end.flatten
      dir_search.each do |dir_path|
        real_path = Pathname.new(File.join(dir_path, partial_path)).cleanpath
        if real_path.exist?
          Kameleon.ui.verbose("Register data #{real_path}")
          @data_files.push(real_path) unless @data_files.include? real_path
          return real_path
        end
        Kameleon.ui.verbose("#{real_path} : nonexistent")
      end
      fail RecipeError, "Cannot find data '#{partial_path}' used in '#{step_path}'"
    end

    def resolve!(kwargs = {})
      Kameleon.ui.verbose("Resolving recipe...")
      unless @global.keys.include? "kameleon_uuid"
        kameleon_id = SecureRandom.uuid
        @global["kameleon_uuid"] = kameleon_id
        @global["kameleon_short_uuid"] = kameleon_id.split("-").last
      end
      # Resolve dynamically-defined variables !!
      resolved_global = Utils.resolve_vars(@global.to_yaml, @path, @global, self, kwargs)
      @global.merge! YAML.unsafe_load(resolved_global)

      consistency_check
      resolve_checkpoint unless @checkpoint.nil?

      Kameleon.ui.verbose("Resolving aliases")
      @sections.values.each do |section|
        section.macrosteps.each do |macrostep|
          # First pass: resolve aliases
          Kameleon.ui.debug("Resolving aliases for macrostep '#{macrostep.name}'")
          macrostep.microsteps.each do |microstep|
            microstep.commands.map! do |cmd|
              resolve_alias(cmd)
            end
            # flatten for multiple-command alias + variables
            microstep.commands.flatten!
          end
        end
      end

      Kameleon.ui.verbose("Resolving variables")
      @sections.values.each do |section|
        section.macrosteps.each do |macrostep|
          macrostep.resolve_variables!(@global, self)
        end
      end

      @sections.values.each do |section|
        section.macrosteps.each do |macrostep|
          # Second pass: resolve variables + clean/init hooks
          macrostep.microsteps.each do |microstep|
            microstep.commands.map! do |cmd|
              resolve_hooks(cmd, macrostep, microstep)
            end
          end
          Kameleon.ui.debug("Compacting macrostep '#{macrostep.name}'")
          # remove empty steps
          macrostep.microsteps.map! do |microstep|
            microstep.commands.compact!
            microstep.commands.empty? ? nil : microstep
          end
          # remove nil values
          macrostep.microsteps.compact!
          Kameleon.ui.debug("Resolving commands for macrostep '#{macrostep.name}'")
          macrostep.microsteps.each do |microstep|
            microstep.resolve!
          end
        end
      end
      calculate_step_identifiers
      flatten_data

      Kameleon.ui.verbose("Recipe is resolved")
    end

    def consistency_check()
      # flatten list of hash to an a hash
      %w(out_context in_context).each do |context_name|
        if @global[context_name].kind_of? Array
          old_context_args = @global[context_name].clone
          @global[context_name] = {}
          old_context_args.each do |arg|
            @global[context_name].merge!(arg)
          end
        end
      end
      Kameleon.ui.verbose("Starting recipe consistency check")
      # check context args
      required_args = %w(cmd)
      missings = []
      %w(out_context in_context).each do |context_name|
        context = @global[context_name]
        missings = required_args - (context.keys() & required_args)
        fail RecipeError, "Required paramater missing for #{context_name}:" \
                          " #{ missings.join ' ' }" unless missings.empty?
      end
      unless @checkpoint.nil?
        required_args = %w(create apply list clear)
        missings = []
        missings = required_args - (@checkpoint.keys() & required_args)
        fail RecipeError, "Required paramater missing for checkpoint:" \
                          " #{ missings.join ' ' }" unless missings.empty?
      end
    end

    def resolve_checkpoint()
      (@checkpoint.keys - ["path"]).each do |key|
        @checkpoint[key].map! do |cmd|
          resolve_alias(cmd)
        end.flatten!
        @checkpoint[key].each do |cmd|
          cmd.string_cmd = Utils.resolve_vars(cmd.string_cmd,
                                              @checkpoint["path"],
                                              @global,
                                              self)
        end
      end
    end

    def resolve_alias(cmd)
      name = cmd.key
      if @aliases.keys.include?(name)
        Kameleon.ui.debug("Resolving alias '#{name}'")
        aliases_cmd = @aliases.fetch(name).clone
        aliases_cmd_str = aliases_cmd.to_yaml
        yaml = YAML.unsafe_load(cmd.string_cmd)
        args = []
        if yaml.is_a?(Hash) # if cmd is an alias with no args, yaml is a String
          args.push(yaml[name]).flatten! # convert args to array
        end
        expected_args_number = aliases_cmd_str.scan(/@\d+/).uniq.count
        if expected_args_number != args.count
          msg = if expected_args_number.zero?
                  "#{name} takes no arguments (#{args.count} given)"
                else
                  "#{name} takes exactly #{expected_args_number} arguments (#{args.count} given)"
                end
          raise RecipeError, msg
        end
        aliases_cmd.map do |c|
          nc = Command.new(c, cmd.microstep_name)
          args.each_with_index do |arg, i|
            nc.gsub!("@#{i + 1}", arg)
          end
          resolve_alias(nc)
        end
      elsif cmd.value.is_a?(Array)
        Kameleon.ui.debug("Search for aliases in the sub-commands of '#{name}'")
        cmd.value.map! { |c| resolve_alias(c) }.flatten!
        cmd.remaster_string_cmd_from_value!
      else
        Kameleon.ui.debug("Leaf command '#{name}' is not an alias")
        cmd
      end
    end

    #handle clean methods
    def resolve_hooks(cmd, macrostep, microstep)
      if (cmd.key =~ /on_(.*)clean/ || cmd.key =~ /on_(.*)init/)
        cmds = []
        if cmd.value.kind_of?(Array)
          cmds = cmd.value.map do |cmd|
            resolve_alias(cmd)
          end
          cmds.flatten!
        else
          fail RecipeError, "Invalid #{cmd.key} arguments"
        end
        if cmd.key.eql? "on_clean"
          microstep_name = "_clean_#{macrostep.clean_microsteps.count}" \
                           "_#{microstep.name}"
          new_clean_microstep = Microstep.new({microstep_name => []})
          new_clean_microstep.on_checkpoint = microstep.on_checkpoint
          new_clean_microstep.commands = cmds.clone
          macrostep.clean_microsteps.unshift new_clean_microstep
          return
        elsif cmd.key.eql? "on_init"
          microstep_name = "_init_#{macrostep.init_microsteps.count}"\
                           "_#{microstep.name}"
          new_init_microstep = Microstep.new({microstep_name=> []},
                                             microstep)
          new_init_microstep.on_checkpoint = microstep.on_checkpoint
          new_init_microstep.commands = cmds.clone
          macrostep.init_microsteps.unshift new_init_microstep
          return
        else
          @sections.values.each do |section|
            section.clean_macrostep
            if cmd.key.eql? "on_#{section.name}_clean"
              microstep_name = "_clean_#{section.clean_macrostep.microsteps.count}" \
                               "_#{microstep.name}"
              new_clean_microstep = Microstep.new({microstep_name=> []})
              new_clean_microstep.commands = cmds.clone
              new_clean_microstep.on_checkpoint = microstep.on_checkpoint
              section.clean_macrostep.microsteps.unshift new_clean_microstep
              return
            elsif cmd.key.eql? "on_#{section.name}_init"
              microstep_name = "_init_#{section.init_macrostep.microsteps.count}" \
                               "_#{microstep.name}"
              new_init_microstep = Microstep.new({microstep_name=> []})
              new_init_microstep.commands = cmds.clone
              new_init_microstep.on_checkpoint = microstep.on_checkpoint
              section.init_macrostep.microsteps.push new_init_microstep
              return
            end
          end
        end
        fail RecipeError, "Invalid command: '#{cmd.key}'"
      else
        return cmd
      end
    end

    def microsteps
      if @microsteps.nil?
        microsteps = []
        @sections.values.each do |section|
          section.sequence do |macrostep|
            macrostep.sequence do |microstep|
              microsteps.push microstep
            end
          end
        end
        @microsteps = microsteps
      end
      return @microsteps
    end

    def all_checkpoints
      if @all_checkpoints.nil?
        @all_checkpoints = []
        microsteps.each do |m|
          step = m.slug
          while @all_checkpoints.map{|c| c["step"]}.include?(step) do
            prefix, suffix = step.split("%")
            step = if suffix.nil?
                     "#{prefix}%1"
                   else
                     "#{prefix}%#{suffix.to_i+1}"
                   end
          end
          @all_checkpoints.push({ 'id' => m.identifier, 'step' => step })
        end
      end
      return @all_checkpoints
    end

    def calculate_step_identifiers
      Kameleon.ui.debug("Calculating microstep identifiers")
      base_salt = ""
      order = 0
      @sections.values.each do |section|
        section.sequence do |macrostep|
          macrostep.sequence do |microstep|
            if ["redo", "skip"].include? microstep.on_checkpoint
              microstep.calculate_identifier ""
            else
              base_salt = microstep.calculate_identifier base_salt
            end
            slug = "#{section.name}/#{macrostep.name}/#{microstep.name}"
            microstep.slug = slug
            microstep.order = (order += 1)
            Kameleon.ui.debug("  #{microstep.slug}: #{microstep.identifier}")
          end
        end
      end
    end

    def flatten_data
      files = []
      @data_files.each do |d|
        if d.directory?
          Find.find("#{d}") do |f|
            files.push(Pathname.new(f)) unless File.directory? f
          end
        else
          files.push(d)
        end
      end
      @data_files = files.uniq
    end


    def to_hash
      recipe_hash = {
        "name" => @name,
        "path" => @path.to_s,
        "base_recipes_files" => @base_recipes_files.map {|p| p.to_s },
        "step_files" => @step_files.map {|p| p.to_s },
        "env_files" => @env_files.map {|p| p.to_s },
        "data_files" => @data_files.map {|p| p.to_s },
        "global" => @global,
        "aliases" => @aliases,
      }
      recipe_hash["checkpoint"] = @checkpoint unless @checkpoint.nil?
      recipe_hash["steps"] = to_array
      return recipe_hash
    end

    def display_info(do_relative_path)
      def prefix
        Kameleon.ui.shell.say " -> ", :magenta
      end
      def relative_or_absolute_path(do_relative_path, path)
        if do_relative_path
          return path.relative_path_from(Pathname(Dir.pwd))
        else
          return path
        end
      end
      Kameleon.ui.shell.say "--------------------"
      Kameleon.ui.shell.say "[Name]", :red
      prefix ; Kameleon.ui.shell.say "#{@name}"
      Kameleon.ui.shell.say "[Path]", :red
      prefix ; Kameleon.ui.shell.say relative_or_absolute_path(do_relative_path, @path), :cyan
      Kameleon.ui.shell.say "[Description]", :red
      prefix ; Kameleon.ui.shell.say "#{@metainfo['description']}"
      Kameleon.ui.shell.say "[Parent recipes]", :red
      (@base_recipes_files - [@path]).each do |base_recipe_file|
        prefix ; Kameleon.ui.shell.say relative_or_absolute_path(do_relative_path, base_recipe_file), :cyan
      end
      Kameleon.ui.shell.say "[Steps]", :red
      @step_files.each do |step|
        prefix ; Kameleon.ui.shell.say relative_or_absolute_path(do_relative_path, step), :cyan
      end
      Kameleon.ui.shell.say "[Data]", :red
      @data_files.each do |d|
        prefix ; Kameleon.ui.shell.say relative_or_absolute_path(do_relative_path, d), :cyan
      end
      Kameleon.ui.shell.say "[Environment scripts]", :red
      @env_files.each do |d|
        prefix ; Kameleon.ui.shell.say relative_or_absolute_path(do_relative_path, d), :cyan
      end
      Kameleon.ui.shell.say "[Variables]", :red
      @global.sort.map do |key, value|
        value = "\n" if value.to_s.empty?
        prefix ; Kameleon.ui.shell.say "#{key}: ", :yellow
        Kameleon.ui.shell.say "#{value}"
      end
    end

    def to_array
      array = []
      @sections.values.each do |section|
        section.to_array.each { |m|  array.push m }
      end
      return array
    end

    def all_files
      return @base_recipes_files + @step_files + @data_files + @env_files
    end

  end

  class RecipeTemplate < Recipe

    def initialize(path, kwargs = {})
      super(path, kwargs)
    end

    def relative_path_from_recipe(recipe_path)
      recipe_path = Pathname.new(recipe_path)
      relative_path_tpl_repo = @path.relative_path_from(Kameleon.env.repositories_path)
      absolute_path = Pathname.new(Kameleon.env.workspace).join(relative_path_tpl_repo)
      return absolute_path.relative_path_from(recipe_path.dirname)
    end
  end
end
