module Movier
  class LMDB
    DataFolder = File.join(ENV['HOME'], ".movier")
    DataFile = File.join(DataFolder, "data.yaml")

    attr_reader :movies, :boxes

    def initialize
      FileUtils.mkdir_p DataFolder
      FileUtils.touch DataFile unless File.exists?(DataFile)
      read_data
    end

    def read_data
      data    = Movier.read_yaml(DataFile)
      @boxes  = data ? data[:boxes]  : []
      @movies = data ? data[:movies] : []
    end

    def write_data(movies = nil, boxes = nil)
      boxes ||= @boxes; movies ||= @movies;
      data = { boxes: boxes, movies: movies }
      Movier.write_yaml DataFile, data
      read_data
    end

    def find_persons(kind = :actors)
      invalid_kind = @movies.first && !@movies.first.has_key?(kind.to_sym)
      raise "I don't have any info about #{Movier.titleize(kind.to_s)}" if invalid_kind
      persons = @params[kind.to_sym]
      if persons
        persons.split(",").each do |person|
          @movies.select!{ |movie| movie[kind.to_sym].join(", ").downcase.include? person.downcase.strip}
        end
      end
    end

    def genre
      genre = [ "Action", "Adventure", "Animation", "Biography", "Comedy",
                "Crime", "Documentary", "Drama", "Family", "Fantasy",
                "Film-Noir", "Game-Show", "History", "Horror", "Music",
                "Musical", "Mystery", "News", "Reality-TV", "Romance",
                "Sci-Fi", "Sport", "Talk-Show", "Thriller", "War", "Western" ]
      Movier.tip_now "Listing all IMDB.com defined genre"
      puts " " * 20 + "=" * 20
      genre.each_slice(6) do |g|
        puts " "*20 + g.join(", ")
      end

    end

    def lookup(id)
      @movies.each do |movie|
        return movie if movie[:id] == id
      end
      false
    end

    def find(params)
      # TODO: add support for "find in a box" and "exclude boxes"
      read_data; @params = params; @params[:verbose] = true if @params[:shuffle]
      raise "Please, add some movie boxes, before searching in the local movie database" unless @movies.any?

      @movies.select!{|movie| movie[:title].downcase.include? @params[:keywords].downcase} if @params[:keywords]
      [:directors, :writers, :actors, :genre, :tags].each { |kind| find_persons(kind) }
      @movies.select!{|movie| movie[:rated] == @params[:rated] } if @params[:rated]
      @movies.select!{|movie| movie[:rating] >= @params[:points].to_f } if @params[:points]

      # filter on tag exclusion
      tags = @params[:exclude_tags]
      if tags
        tags.split(",").each do |tag|
          @movies.reject!{ |movie| movie[:tags].join(", ").downcase.include? tag.downcase.strip}
        end
      end
      @movies.reject!{|movie| movie[:tags].join(", ").downcase.include? "watched"} unless @params[:tags] &&
        @params[:tags].include?("watched")

      sort_movies
      @movies = @movies.slice(0, @params[:limit].to_i) if @params[:limit].to_i > 0
      @movies = [ @movies.shuffle.first ] if @params[:shuffle]

      counter = 1
      @movies.each do |movie|
        nice_name = "#{movie[:title]} [#{movie[:year]}]"
        if @params[:verbose]
          Movier.tip_now "%03d" % counter + ") " + nice_name, Movier.titleize(movie[:type])
          Movier.tip_now movie[:path], "Path"
          rating = "#{movie[:rated]} at #{movie[:rating]} points."
          Movier.say_rated "Rating", rating, movie[:rating]
          Movier.say_rated "Votes", "#{movie[:votes]} IMDB.com votes", movie[:rating]
          Movier.say_rated "Genre", movie[:genre].join(", "), movie[:rating], true
          Movier.say_rated "Runtime", movie[:runtime], movie[:rating]
          Movier.say_rated "Directors", movie[:directors].join(", "), movie[:rating], true
          Movier.say_rated "Actors", movie[:actors].join(", "), movie[:rating], true
          Movier.say_rated "Writers", movie[:writers].join(", "), movie[:rating], true if @params[:writer]
          Movier.say_rated "Plot", movie[:plot], movie[:rating], true
          Movier.say_rated "Tags", movie[:tags].join(", "), movie[:rating] if movie[:tags].any?
        else
          message = "#{"%03d" % counter}) #{nice_name}"
          Movier.say_rated Movier.titleize(movie[:type]), message, movie[:rating]
          rating = "#{movie[:rated]} at #{movie[:rating]} points with #{movie[:votes]} votes."
          Movier.say_rated "Rating", rating, movie[:rating]
          Movier.say_rated "Directors", movie[:directors].join(", "), movie[:rating], true if @params[:directors]
          Movier.say_rated "Actors", movie[:actors].join(", "), movie[:rating], true
          Movier.say_rated "Tags", movie[:tags].join(", "), movie[:rating] if movie[:tags].any?
        end
        puts
        counter += 1
      end
      return if @movies.empty?
      filtered = @movies

      # add some tags to this search
      if @params[:add_tags]
        tags = @params[:add_tags].split(",").map{|t| Movier.titleize(t.strip)}
        read_data
        filtered.each do |f|
          @movies.each_with_index do |m,i|
            @movies[i][:tags] |= tags if m[:id] == f[:id]
          end
        end
        write_data
        Movier.tip_now "Added tags: '#{tags.join(", ")}' to this search!"
      end

      # TODO: remove tags?

      message = "Do you want me to play #{@params[:shuffle] ? "this" : "some"} movie for you? [enter number from above] "
      begin; open = ask(message) {|x| x.default = "no" }; rescue Exception; end
      if open == "no" && @params[:shuffle]
        find @params
      elsif open && open.to_i > 0
        movie = filtered[open.to_i - 1]
        # TODO: fix this to use a pure ruby implementation
        require 'shellwords'
        nice_name = "#{movie[:title]} [#{movie[:year]}]"
        movie_dir = Shellwords.escape(movie[:path])
        movie_file = `find #{movie_dir} -type f`.strip.split("\n")
        movie_file = movie_file.select{|f| File.size(f) > 100 * 2**20}.first
        Movier.tip_now "Opening: #{nice_name} with VLC Player"
        `open '#{movie_file}' -a VLC &`
      end
    end

    def sort_movies
      @movies = @movies.sort_by {|movie| movie[:weight] }.reverse
    end

    # Update the local movie database,
    # by revisiting all tracked directories,
    # and building the database from there.
    #
    def update_itself
      message  = "Found no movie box in the local database.\n"
      message += "Please, run `movier add` to add some movie boxes, before updating me!"
      raise message if @boxes.empty?
      @movies = []
      write_data
      @boxes.each { |box| add(box) }
    end

    # Add a given directory to the local movie database.
    # The directory should be pre-organized using the Organize command.
    #
    # * *Args*    :
    #   - +dir+ -> directory to add to the local movie database
    #
    def add(dir = nil)
      dir = File.expand_path dir
      raise "No such directory!" unless File.directory?(dir)

      imdb = Dir.glob("#{dir}/**/imdb.txt")
      raise 'You should first run `movier organize` on this directory!' unless imdb.count > 0

      count = 0
      imdb.each do |file|
        movie = Movier.read_yaml file
        if in_database?(movie)
          # TODO: add the path with a "dup" key
          Movier.warn_with "#{movie[:imdb]["Title"]} - # #{movie[:imdb]["imdbRating"]} -  already exists in database!"
        elsif !in_database?(movie)
          @movies.push sanitize(movie, dir, file)
          count += 1
        end
      end

      @boxes.push dir unless @boxes.include? dir
      write_data

      Movier.passed_with "Added #{"%4d" % count} new movies in LMDB from: #{dir}"
      Movier.tip_now "LMDB now contains #{"%4d" % @movies.count} movies."
    end

    def sanitize(movie, dir, imdb_file)
      imdb = movie[:imdb]
      nice_name = "#{imdb["Title"]} [#{imdb["Year"]}]"
      hash = (Digest::MD5.new << nice_name).to_s.slice(0,8)
      data = {
        title:      imdb["Title"],
        year:       imdb["Year"],
        rated:      imdb["Rated"],
        released:   imdb["Released"],
        runtime:    imdb["Runtime"],
        genre:      make_parts(imdb["Genre"]),
        directors:  make_parts(imdb["Director"]),
        writers:    make_parts(imdb["Writer"]),
        actors:     make_parts(imdb["Actors"]),
        plot:       imdb["Plot"],
        poster:     imdb["Poster"],
        _poster:    File.join(dir, nice_name, "poster#{File.extname(imdb["Poster"])}"),
        rating:     imdb["imdbRating"].to_f,
        votes:      imdb["imdbVotes"].to_s.delete(",").to_i,
        type:       imdb["Type"],
        id:         imdb["imdbID"],
        hash:       hash.force_encoding("UTF-8"),
        tags:       [],
        box:        dir,
        path:       File.dirname(imdb_file)
      }
      data[:weight] = (data[:rating] * data[:votes]).to_i
      data
    end

    def make_parts(string, delimiter = ",")
      string.split(delimiter).map{|x| x.strip}
    end

    def in_database?(movie)
      @movies.select{|m| m[:id] == movie[:imdb]["imdbID"]}.any?
    end
  end

  def self.read_yaml(file)
    YAML.load_file(file)
  end

  def self.write_yaml(file, data)
    File.open(file, "w") {|f| f.puts data.to_yaml }
  end
end