# Less::More provides methods for parsing LESS files in a rails application to CSS target files.
# 
# When Less::More.parse is called, all files in Less::More.source_path will be parsed using LESS
# and saved as CSS files in Less::More.destination_path. If Less::More.compression is set to true,
# extra line breaks will be removed to compress the CSS files.
#
# By default, Less::More.parse will be called for each request in `development` environment and on
# application initialization in `production` environment.

begin
  require 'less'
rescue LoadError => e
  e.message << " (You may need to install the less gem)"
  raise e
end

class Less::More
  HEADER = %{/*\n\n\n\n\n\tThis file was auto generated by Less (http://lesscss.org). To change the contents of this file, edit %s instead.\n\n\n\n\n*/}
  
  class << self
    # Less::More.compression = true/false --- compress generated css ? (default: false)
    # Less::More.header = true/false --- insert editing warning into css ? (default: true)
    # Less::More.destination_path = 'css' --- put css into public/??? (default: stylesheets)
    # Less::More.source_path = 'public/stylesheets/less' --- where do less files live? (default: app/stylesheets)
    attr_writer :compression, :header, :destination_path, :source_path

    def header
      @header.nil? ? true : @header
    end

    def destination_path
      @destination_path || File.join(Rails.root, 'public', 'stylesheets')
    end

    def source_path
      @source_path || File.join(Rails.root, 'app', 'stylesheets')
    end

    def compression
      @compression
    end

    # Generates the .css from a .less or .lss file in Less::More.source_path matching
    # the given parameters.
    #
    #   Less::More.generate("screen.less")
    #   Less::More.generate("subdirectories/here/homepage.less")
    def generate(source)
      generated = to_dot_css(path_to_destination(source))
      path_to_source = File.join(source_path, source)

      # check if the destination file exists, and compare the modified times to see if it needs to be written
      if mtime(generated) >= mtime_including_imports(path_to_source)
        # up to date, nothing to do!
      else
        # css file does not exist or is out of date
        css = if File.extname(path_to_source) == ".css"
          # vanilla css nothing to do!
          File.read(path_to_source)
        else
          # less or lss file, compile it
          css = compile(path_to_source)
          css.delete!("\n") if compression # TODO: use real compression !
          css = (HEADER % [File.join(source_path, source)]) << css if header
          css
        end

        # write the css
        FileUtils.mkdir_p File.dirname(generated)
        File.open(generated, "w"){|f| f.write css }
      end
    end
    
    # Generates all the .css files
    def generate_all
      all_less_files.each do |path|
        generate(relative_to_source_path(path))
      end
    end
    
    # Removes all generated css files.
    def remove_all_generated
      all_less_files.each do |path|
        css_path  = to_dot_css(relative_to_source_path(path))
        css_file = path_to_destination(css_path)
        File.delete(css_file) if File.file?(css_file)
      end
    end

    # Array of paths of less source files.
    def all_less_files
      all = Dir[File.join(source_path, "**", "*.{css,less,lss}")]
      all.reject{|path| File.basename(path) =~ /^_/ }
    end

    private

    def mtime(file)
      return 0 unless File.file?(file)
      File.mtime(file).to_i
    end

    # consider imports for mtime
    # just 1 level deep so we do not get any looping/nesting errors
    def mtime_including_imports(file)
      mtimes = [mtime(file)]
      File.readlines(file).each do |line|
        if line =~ /^\s*@import ['"]([^'"]+)/
          imported = File.join(File.dirname(file), $1)
          mtimes << if imported =~ /\.le?ss$/ # complete path given ?
            mtime(imported)
          else # we need to add .less or .lss
            [mtime("#{imported}.less"), mtime("#{imported}.lss")].max
          end
        end
      end
      mtimes.max
    end

    def compile(file)
      begin
        parser = Less::Parser.new :paths => [source_path]
        tree   = parser.parse File.read(file)
        tree.to_css
      rescue Exception => e
        e.message << "\nFrom #{file}"
        raise e
      end
    end

    def to_dot_css(path)
      path.to_s.sub(/(le?|c)ss$/, "css")
    end

    def path_to_destination(path)
      File.join(destination_path, path)
    end

    def relative_to_source_path(path)
      File.expand_path(path.to_s).sub(File.expand_path(source_path), '')[1..-1]
    end
  end
end
