module Hyde
  # TODO: This class is growing... time to refactor
  class Project
    include Hyde::Utils

    # The filename of the configuration file, relative to the project root. (String)
    #
    attr :config_file

    # The configuration k/v storage (OStruct)
    #
    attr_accessor :config

    # The root path (String).
    #
    # @example
    #   root
    #   root :layouts
    #   root :site, 'blah'
    #
    def root(*args)
      where = ''
      where = @config.send("#{args.shift.to_s}_path")  if args[0].class == Symbol
      path = args
      File.expand_path File.join([@root, where, path].reject { |s| s.empty? })
    end

    # Can raise a NoRootError
    #
    def initialize(root = Dir.pwd)
      @config = OStruct.new defaults
      @root, @config_file = find_root_from root
      @config.merge! YAML::load_file(@config_file)

      # Check for version
      raise IncompatibleError, "This project requires at least Hyde v#{@config.hyde_requirement}." \
        unless @config.hyde_requirement.nil? or \
          ::Hyde.compatible_with?(@config.hyde_requirement)

      load_extensions
    end

    # Returns an object in the project.
    # Can be a page, layout, template...
    #
    # @example
    #   @project['index.html']
    #   @project['default', :Layout]
    #   @project['widgets/sidebar', :Partial]
    #
    def [](name, default_class=Page)
      default_class = ::Hyde.const_get(default_class)  if default_class.is_a? Symbol
      Hyde::Page[name, self, default_class]
    end

    # Returns all pages.
    #
    # @example
    #   @project.all                #=> [1, 2, 3]
    #   @project.all(Hyde::Post)    #=> [1]
    #
    def all(type=nil)
      unless type
        @all = files.map { |f| self[f] }
      else
        all.select { |page| page.class == type }
      end
    end

    # Writes the output files.
    # @param
    #   ostream    - (Stream) Where to send the messages
    #
    def build(ostream = nil)
      if File.exists? root(:output)
        raise Errno::EEXISTS  unless File.directory? root(:output)
      else
        Dir.mkdir root(:output)
      end

      begin
        all.each do |page|
          ostream << " * #{page.output_path}\n"  if ostream

          begin
            rendered = page.render
            force_file_open(page.output_path) { |file| file << rendered }

          rescue RenderError => e
            ostream << " *** Error: #{e.to_s}".gsub("\n", "\n *** ") << "\n"
          end
        end

      rescue NoGemError => e
        ostream << " *** Error: #{e.message}\n"
      end
    end

    # Returns a list of all URL paths
    #
    def files
      @files = Dir[File.join(root(:site), '**', '*')].inject([]) do |a, match|
        # Make sure its the canonical name
        path = File.expand_path(match)
        file = path.gsub /^#{Regexp.escape root(:site)}\/?/, ''
        ext  = File.extname(file)[1..-1]
        
        if ignored_files.include?(path) or File.directory?(match)
          # pass
        elsif not get_renderer(ext).nil? # Has a renderer associated
          fname = file.chomp(".#{ext}")
          fname += get_renderer(ext).default_ext  unless File.basename(fname).include?('.')
          a << fname
        else
          a << file
        end
        a
      end
    end

    # Returns a list of file specs to be excluded from processing.
    # @see {#ignored_files}
    #
    def ignore_list
      @ignore_list ||= [
        root(:layouts, '**/*'),
        root(:extensions, '**/*'),
        root(:output, '**/*'),
        custom_ignored_files,
        @config_file
      ].flatten.uniq
    end

    def custom_ignored_files
      list = @config.send("ignore")
      return []  unless list.is_a?(Array)
      Dir[*(list.map { |path| root(:site, path) })]
    end

    # Returns a list of ignored files based on the {ignore_list}.
    # TODO: This is innefficient... do it another way
    #
    def ignored_files
      @ignored_files ||= ignore_list.inject([]) do |a, spec|
        Dir[spec].each { |file| a << File.expand_path(file) }; a
      end
    end

    def self.config_filenames
      ['_config.yml', 'hyde.conf', '.hyderc']
    end

    protected

    # Looks for the hyde config file to determine the project root.
    #
    def find_root_from(start)
      check = File.expand_path(start)
      ret = nil
      while ret.nil?
        # See if any of these files exist
        self.class.config_filenames.each do |config_name|
          config_file = File.join(check, config_name)
          ret ||= [check, config_file]  if File.exists? config_file
        end

        # Traverse back (die if we reach the root)
        old_check = check
        check = File.expand_path(File.join(check, '..'))
        raise NoRootError  if check == old_check
      end
      ret
    end

    # Loads the ruby files in the extensions folder
    #
    def load_extensions
      # Load the init.rb file
      require root('init.rb')  if File.exists?(root('init.rb'))

      # Load the gems in the config file
      @config.gems.each { |gem| require gem }

      # Load the extensions
      ext_roots = Dir[root(:extensions, '*')].select { |d| File.directory? d }
      ext_roots.each do |dir|
        ext = File.basename(dir)

        # Try extensions/name/name.rb
        # Try extensions/name/lib/name.rb
        ext_files = [
          File.join(dir, "#{ext}.rb"),
          File.join(dir, 'lib', "#{ext}.rb")
        ]
        ext_files.reject! { |f| not File.exists? f }
        require ext_files[0]  if ext_files[0]
      end
    end

    def defaults
      { 'layouts_path'    => 'layouts',
        'extensions_path' => 'extensions',
        'partials_path'   => 'layouts',
        'site_path'       => 'site',
        'output_path'     => 'public',
        'port'            => 4833,
        'gems'            => []
      }
    end

    # Returns the renderer associated with the given file extension.
    #
    def get_renderer(name)
      begin
        class_name = name.to_s.capitalize.to_sym
        renderer = ::Hyde::Renderers.const_get(class_name)
      rescue NameError
        renderer = nil
      end
      renderer
    end
  end
end
