require 'jasmine-core'
require 'time'
require 'multi_json'
require 'set'
require 'sprockets'
require 'sprockets/engines'
require 'sprockets-vendor_gems'

module Jasmine::Headless
  class FilesList
    include FileChecker

    class << self
      def asset_paths
        @asset_paths ||= Sprockets.find_gem_vendor_paths(:for => 'javascripts')
      end

      def reset!
        @asset_paths = nil
        @registered_engines = {}

        # register haml-sprockets and handlebars_assets if it's available...
        %w{haml-sprockets handlebars_assets}.each do |library|
          begin
            require library
          rescue LoadError
          end
        end

        if ENV['JHW_ENV']
          begin
            require 'bundler'

            Bundler.require(ENV['JHW_ENV'].to_sym)
          rescue LoadError
          end
        end

        @sprockets_environment = nil
      end

      def registered_engines
        @registered_engines ||= {}
      end

      def register_engine(file_extension, template_class)
        registered_engines[file_extension] = template_class
      end

      def register_engines!
        registered_engines.each do |file_extension, template_class|
          Sprockets.register_engine file_extension, template_class
        end
      end

      def default_files
        %w{jasmine.js jasmine-html jasmine.css jasmine-extensions
           intense headless_reporter_result jasmine.HeadlessReporter
           jasmine.HeadlessReporter.ConsoleBase
           jsDump beautify-html}
      end

      def extension_filter
        extensions = (%w{.js .css} + Sprockets.engine_extensions)

        %r{(#{extensions.join('|')})$}
      end
    end

    PLEASE_WAIT_IM_WORKING_TIME = 2

    attr_reader :options, :required_files, :potential_files_to_filter

    def initialize(options = {})
      @options = options

      Kernel.srand(options[:seed]) if options[:seed]

      @required_files = UniqueAssetList.new
      @potential_files_to_filter = []

      register_engines!

      load_initial_assets

      use_config if config?
    end

    def register_engines!
      begin
        require spec_helper
      rescue LoadError
      end
      self.class.register_engines!
    end

    def load_initial_assets
      self.class.default_files.each do |file|
        begin
          add_path(file)
        rescue InvalidUniqueAsset => e
          raise StandardError.new("Not an asset: #{file}")
        end
      end

      (options[:reporters] || []).each do |reporter, identifier, file|
        add_path("jasmine.HeadlessReporter.#{reporter}")
      end
    end

    def files
      required_files.flatten.collect { |asset| asset.pathname.to_s }.uniq
    end

    def spec_files
      filter_for_requested_specs(
        files.find_all { |file| spec_dir.any? { |dir| file[dir] } }
      )
    end

    def filtered_files
      filter_for_requested_specs(files)
    end

    def search_paths
      return @search_paths if @search_paths

      @search_paths = [ Jasmine::Core.path, Jasmine::Headless.root.join('vendor/assets/javascripts').to_s ]
      @search_paths += self.class.asset_paths
      @search_paths += src_dir.collect { |dir| File.expand_path(dir) }
      @search_paths += asset_paths.collect { |dir| File.expand_path(dir) }
      @search_paths += spec_dir.collect { |dir| File.expand_path(dir) }

      @search_paths.uniq!
      @search_paths
    end

    def sprockets_environment
      return @sprockets_environment if @sprockets_environment

      @sprockets_environment = Sprockets::Environment.new
      search_paths.each { |path| @sprockets_environment.append_path(path) }

      @sprockets_environment.unregister_postprocessor('application/javascript', Sprockets::SafetyColons)

      # ...and unregister ones we don't want/need
      @sprockets_environment.instance_eval do
        EXCLUDED_FORMATS.each do |extension|
          register_engine ".#{extension}", Jasmine::Headless::NilTemplate
        end

        register_engine '.coffee', Jasmine::Headless::CoffeeTemplate
        register_engine '.js', Jasmine::Headless::JSTemplate
        register_engine '.css', Jasmine::Headless::CSSTemplate
        register_engine '.jst', Jasmine::Headless::JSTTemplate
      end

      @sprockets_environment
    end

    def has_spec_outside_scope?
      if is_outside_scope = !spec_filter.empty?
        is_outside_scope = spec_dir.any? do |dir|
          spec_file_searches.any? do |search|
            !spec_files.any? do |file|
              target = File.join(dir, search)
              File.fnmatch?(target, file) || File.fnmatch?(target.gsub(%{^**/}, ''), file)
            end
          end
        end
      end

      is_outside_scope
    end

    def filtered?
      files != filtered_files
    end

    def files_to_html
      to_html(files)
    end

    def filtered_files_to_html
      to_html(filtered_files)
    end

    def spec_file_line_numbers
      @spec_file_line_numbers ||= Hash[spec_files.collect { |file|
        if File.exist?(file)
          if !(lines = Jasmine::Headless::SpecFileAnalyzer.for(file)).empty?
            [ file, lines ]
          end
        else
          nil
        end
      }.compact]
    end

    private
    def to_html(files)
      alert_time = Time.now + PLEASE_WAIT_IM_WORKING_TIME

      files.collect do |file|
        if alert_time && alert_time < Time.now
          puts "Rebuilding cache, please wait..."
          alert_time = nil
        end

        sprockets_environment.find_asset(file, :bundle => false).body
      end.compact.reject(&:empty?)
    end

    def spec_filter
      @spec_filter ||= (@options[:only] && @options[:only].collect { |path| expanded_dir(path) }.flatten) || []
    end

    SEARCH_ROOTS = {
      'src_files' => 'src_dir',
      'stylesheets' => 'src_dir',
      'helpers' => 'spec_dir',
      'spec_files' => 'spec_dir'
    }

    def use_config
      @config = @options[:config].dup
      @searches = {}
      @potential_files_to_filter = []

      %w{src_files stylesheets helpers spec_files}.each do |type|
        if data = @config[type]
          add_files(@searches[type] = data.flatten, type, send(SEARCH_ROOTS[type]))
        end
      end
    end

    def add_files(patterns, type, dirs)
      patterns.each do |pattern|
        dirs.collect { |dir| expanded_dir(File.join(dir, pattern)) }.each do |files|
          files.sort! { |a, b| Kernel.rand(3) - 1 } if type == 'spec_files'

          files.each do |path|
            add_path(path, type)
          end
        end
      end

      if type == 'spec_files'
        spec_filter.each { |path| add_path(path, type) }
      end
    end

    def config?
      @options[:config]
    end

    def expanded_dir(path)
      file_list = Dir.glob(path).sort
      file_list.find_all { |file|
        file[extension_filter] && !alert_if_bad_format?(file)
      }.collect {
        |file| File.expand_path(file)
      }.find_all {
        |path| File.file?(path)
      }
    end

    def extension_filter
      self.class.extension_filter
    end

    def add_path(path, type = nil)
      asset = sprockets_environment.find_asset(path)

      @required_files << asset

      if type == 'spec_files'
        @potential_files_to_filter << path
      end
    end

    def src_dir
      @src_dir ||= config_dir_or_pwd('src_dir') + asset_paths
    end

    def spec_dir
      @spec_dir ||= config_dir_or_pwd('spec_dir')
    end

    def asset_paths
      @asset_paths ||= config_dir('asset_paths')
    end

    def spec_file_searches
      @searches['spec_files']
    end

    def config_dir_or_pwd(dir)
      if (found = config_dir(dir)).empty?
        found = [ Dir.pwd ]
      end

      found
    end

    def config_dir(dir)
      [ @options[:config] && @options[:config][dir] ].flatten.compact.collect { |dir| File.expand_path(dir) }
    end

    def filter_for_requested_specs(files)
      files.find_all do |file|
        if potential_files_to_filter.include?(file)
          spec_filter.empty? || spec_filter.any? { |pattern| File.fnmatch?(pattern, file) }
        else
          true
        end
      end
    end

    def spec_helper
      File.join(spec_dir, "helpers", "spec_helper")
    end
  end
end

module Jasmine::Headless
  extend self

  def register_engine(file_extension, template_class)
    Jasmine::Headless::FilesList.register_engine(file_extension, template_class)
  end
end
