# -*- coding: utf-8 -*-

require 'find'
require 'rubygems'
# require 'termcolor'
require 'kconv'
require 'milkode/common/platform'
require 'milkode/common/grenfiletest'
require 'milkode/common/grensnip'
require 'milkode/common/util'
include Gren
require 'cgi'
require 'pathname'
require 'milkode/database/groonga_database'

module Milkode
  class FindGrep
    class Result
      attr_accessor :count
      attr_accessor :search_count
      attr_accessor :match_file_count
      attr_accessor :match_count
      attr_accessor :size
      attr_accessor :search_size
      
      attr_accessor :search_files
      attr_accessor :match_files
      attr_accessor :unreadable_files
      attr_accessor :prune_dirs
      attr_accessor :ignore_files

      def initialize(start_dir)
        @start_dir = File.expand_path(start_dir)
        @count, @search_count, @match_file_count, @match_count, @size, @search_size = 0, 0, 0, 0, 0, 0
        @start_time = Time.now
        @search_files, @match_files, @unreadable_files, @prune_dirs, @ignore_files  = [], [], [], [], []
      end

      def time_stop
        @end_time = Time.now
      end

      def time
        @end_time - @start_time 
      end

      def print(stdout)
        stdout.puts "dir   : #{@start_dir} (#{Gren::Util.time_s(time)})"
        stdout.puts "files : #{@search_count} in #{@count} (#{Gren::Util.size_s(@search_size)} in #{Gren::Util.size_s(@size)})"
        stdout.puts "match : #{@match_file_count} files, #{match_count} hit"
      end
    end

    class MatchCountOverError < RuntimeError ; end

    attr_reader :documents
    
    def initialize(patterns, option)
      @patterns       = patterns
      @option         = option
      @patternRegexps = strs2regs(patterns)
      @subRegexps     = strs2regs(option.patternsNot)
      @orRegexps      = strs2regs(option.patternsOr)
      @filePatterns   = (!@option.dbFile) ? strs2regs_simple(option.filePatterns) : []
      @ignoreFiles    = strs2regs_simple(option.ignoreFiles)
      @ignoreDirs     = strs2regs_simple(option.ignoreDirs)
      @result         = Result.new(option.directory)
      open_database       if @option.dbFile
      require 'termcolor' if @option.colorHighlight
    end

    def open_database()
      # データベースファイル
      dbfile = Pathname(File.expand_path(@option.dbFile))

      # データベース開く
      if dbfile.exist?
        if !@grndb || @grndb.closed?
          @grndb = Milkode::GroongaDatabase.new
          @grndb.open_file(dbfile.to_s)
          @documents = @grndb.documents
          puts "open    : #{dbfile.to_s} open." unless @option.isSilent
        end
      else
        raise "error    : #{dbfile.to_s} not found!!"
      end
    end

    def strs2regs(strs)
      regs = []

      strs.each do |v|
        option = 0
        option |= Regexp::IGNORECASE if (@option.ignoreCase || (!@option.caseSensitive && Milkode::Util.downcase?(v)))
        regs << Regexp.new(Regexp.escape(v), option)
      end

      regs
    end

    def strs2regs_simple(strs)
      regs = []

      strs.each do |v|
        regs << Regexp.new(Regexp.escape(v), 0)
      end

      regs
    end

    def searchAndPrint(stdout)
      records = searchDatabase
      searchAndPrint2(stdout, records)
    end

    def searchAndPrint2(stdout, records)
      unless (@option.dbFile)
        searchFromDir(stdout, @option.directory, 0)
      else
        searchFromDB(stdout, records, @option.directory)
      end

      @result.time_stop
      
      if (!@option.isSilent && !@option.dispHtml)
        if (@option.debugMode)
          stdout.puts
          stdout.puts "--- search --------"
          print_fpaths stdout, @result.search_files
          stdout.puts "--- match --------"
          print_fpaths stdout, @result.match_files
          stdout.puts "--- ignore-file --------"
          print_fpaths stdout, @result.ignore_files
          stdout.puts "--- ignore-dir --------"
          print_fpaths stdout, @result.prune_dirs
          stdout.puts "--- unreadable --------"
          print_fpaths stdout, @result.unreadable_files
        end

        unless (@option.colorHighlight)
          stdout.puts
        else
          stdout.puts HighLine::REVERSE + "------------------------------------------------------------" + HighLine::CLEAR
        end

        @result.print(stdout)
      end
    end

    def pickupRecords
      raise unless @option.dbFile
      records = searchDatabase
      @result.time_stop
      records
    end

    def time_s
      Gren::Util.time_s(@result.time)
    end

    def searchFromDB(stdout, records, dir)
      # ヒットしたレコード数
      stdout.puts "Found   : #{records.size} records." if (!@option.dispHtml && !@option.isSilent)

      # 検索にヒットしたファイルを実際に検索
      begin
        if (@option.gotoline > 0)
          records.each do |record|
            if FileTest.exist?(record.path)
              relative_path = convert_path(record.path)
              line = getTextLineno(relative_path, @option.gotoline)
              stdout.puts "#{relative_path}:#{@option.gotoline}:#{line}" if (line)
              @result.match_file_count += 1
              raise MatchCountOverError if (0 < @option.matchCountLimit && @option.matchCountLimit <= @result.match_file_count)
            end
          end
        elsif (@patterns.size > 0)
          records.each do |record|
            if (@option.groongaOnly)
              searchGroongaOnly(stdout, record)
            else
              searchFile(stdout, record.path, record.path) if FileTest.exist?(record.path)
            end
          end
        else
          records.each do |record|
            path = record.path
            stdout.puts convert_path(path)
            @result.match_file_count += 1
            raise MatchCountOverError if (0 < @option.matchCountLimit && @option.matchCountLimit <= @result.match_file_count)
          end
        end
      rescue MatchCountOverError
      end
    end

    def getTextLineno(path, no)
      index = no - 1

      open(path, "r") do |file|
        lines = file2data(file)

        if (index < lines.size)
          lines[index]
        else
          nil
        end
      end
    end

    def searchDatabase
      @documents.search(
        :patterns  => @patterns,
        :keywords  => @option.keywords,
        :paths     => @option.filePatterns,
        :packages  => @option.packages,
        :strict_packages  => @option.strict_packages,
        # :restpaths => ,
        :suffixs   => @option.suffixs
        # :offset    => ,
        # :limit     => ,
      )
    end

    def searchFromDir(stdout, dir, depth)
      if (@option.depth != -1 && depth > @option.depth)
        return
      end
      
      Dir.foreach(dir) do |name|
        next if (name == '.' || name == '..')
          
        fpath = File.join(dir,name)
        fpath_disp = fpath.gsub(/^.\//, "")
        
        # 除外ディレクトリならばパス
        if ignoreDir?(fpath)
          @result.prune_dirs << fpath_disp if (@option.debugMode)
          next;
        end

        # 読み込み不可ならばパス
        unless FileTest.readable?(fpath)
          @result.unreadable_files << fpath_disp if (@option.debugMode)
          next
        end

        # ファイルならば中身を探索、ディレクトリならば再帰
        case File.ftype(fpath)
        when "directory"
          searchFromDir(stdout, fpath, depth + 1)
        when "file"
          searchFile(stdout, fpath, fpath_disp)
        end
      end
    end
    private :searchFromDir

    def print_fpaths(stdout, data)
      stdout.print data.join("\n")
      stdout.puts if data.count > 0
      stdout.puts "total: #{data.count}"
      stdout.puts
    end
    private :print_fpaths

    def ignoreDir?(fpath)
      FileTest.directory?(fpath) &&
      (GrenFileTest::ignoreDir?(File.basename(fpath)) || ignoreDirUser?(fpath))
    end
    private :ignoreDir?

    def ignoreDirUser?(fpath)
      @ignoreDirs.any? {|v| v.match File.basename(fpath) }
    end
    private :ignoreDirUser?

    def ignoreFile?(fpath)
      !correctFileUser?(fpath) ||
      GrenFileTest::ignoreFile?(fpath) ||
      ignoreFileUser?(fpath) ||
      GrenFileTest::binary?(fpath)
    end
    private :ignoreFile?

    def correctFileUser?(fpath)
      @filePatterns.empty? ||
      @filePatterns.any? {|v| v.match File.basename(fpath) }
    end
    private :correctFileUser?

    def ignoreFileUser?(fpath)
      @ignoreFiles.any? {|v| v.match File.basename(fpath) }
    end
    private :ignoreFileUser?

    def searchFile(stdout, fpath, fpath_disp)
      @result.count += 1
      @result.size += FileTest.size(fpath)

      # 除外ファイル
      if ignoreFile?(fpath)
        @result.ignore_files << fpath_disp if (@option.debugMode)
        return
      end
      
      @result.search_count += 1
      @result.search_size += FileTest.size(fpath)

      @result.search_files << fpath_disp if (@option.debugMode)

      open(fpath, "r") do |file|
        searchData(stdout, file2data(file), fpath_disp)
      end
    end
    private :searchFile

    def searchGroongaOnly(stdout, record)
      file_size = record.content.size
      
      @result.count += 1
      @result.size += file_size
      
      @result.search_count += 1
      @result.search_size += file_size
      
      @result.search_files << record.path if (@option.debugMode)

      searchData(stdout, record.content.split("\n"), record.path)
    end
    private :searchGroongaOnly

    def searchData(stdout, data, path)
      match_file = false

      data.each_with_index { |line, index|
        result, match_datas = match?(line)

        if ( result )
          unless (@option.dispHtml)
            # header = "#{path}:#{index + 1}:"
            header = "#{convert_path(path)}:#{index + 1}:"
            
            line = GrenSnip::snip(line, match_datas) unless (@option.noSnip)

            if @option.output_kcode != Kconv::UTF8
              header = Kconv.kconv(header, @option.output_kcode)
              line   = Kconv.kconv(line, @option.output_kcode)
            end

            unless (@option.colorHighlight)
              stdout.puts header + line
            else
              stdout.puts HighLine::BLUE + header + HighLine::CLEAR + GrenSnip::coloring(line, match_datas)
            end
          else
            line_no = index + 1
            line = GrenSnip::snip(line, match_datas) unless (@option.noSnip)
            
            stdout.puts <<EOF
<h2><a href="../::view#{path}">#{path}</a></h2>
<pre>
#{line_no} : #{CGI.escapeHTML(line)}
</pre>
EOF
          end

          unless match_file
            @result.match_file_count += 1
            @result.match_files << path if (@option.debugMode)
            match_file = true
            break if (@option.isMatchFile)
          end

          @result.match_count += 1
         if (0 < @option.matchCountLimit && @option.matchCountLimit <= @result.match_count)
           raise MatchCountOverError
         end
        end
      }
    end
    private :searchData

    def file2data(file)
      FindGrep::file2lines(file, @option.kcode)
    end
    private :file2data
    
    def self.file2lines(file, kcode)
      data = file.read
      
      unless Milkode::Util.ruby19?
        if (kcode != Kconv::NOCONV)
          file_kcode = Kconv::guess(data)

          if (file_kcode != kcode)
            # puts "encode!! #{fpath} : #{kcode} <- #{file_kcode}"
            data = data.kconv(kcode, file_kcode)
          end
        end
      else
        # @memo ファイルエンコーディングに相違が起きている可能性があるため対策
        #       本当はファイルを開く時にエンコーディングを指定するのが正しい

        # 方法1 : 強制的にバイナリ化
        # data.force_encoding("Binary")
        # data = data.kconv(kcode)
        
        # 方法2 : 入力エンコーディングを強制的に指定
        data = data.kconv(kcode, Kconv::guess(data))
      end

      data = data.split($/)
    end
    
    def match?(line)
      match_datas = []
      @patternRegexps.each {|v| match_datas << v.match(line)}

      sub_matchs = []
      @subRegexps.each {|v| sub_matchs << v.match(line)}

      or_matchs = []
      @orRegexps.each {|v| or_matchs << v.match(line)}
      
      unless (@option.isMatchFile)
        result = match_datas.all? && !sub_matchs.any? && (or_matchs.empty? || or_matchs.any?)
      else
        result = first_condition(match_datas, sub_matchs, or_matchs)
      end
      result_match = match_datas + or_matchs
      result_match.delete(nil)

      return result, result_match
    end
    private :match?

    def first_condition(match_datas, sub_matchs, or_matchs)
      unless match_datas.empty?
        match_datas[0]
      else
        or_matchs[0]
      end
    end

    def convert_path(path)
     unless @option.expand_path
       Milkode::Util.relative_path(path, Dir.pwd).to_s
     else
       path
     end
    end
  end
end
