require "sqlite3"
require "./util/*"

class Queue
  abstract class Downloader
    property stopped = false
    @library_path : String = Config.current.library_path
    @downloading = false

    def initialize
      @queue = Queue.default
      @queue << self

      spawn do
        loop do
          sleep 1.second
          next if @stopped || @downloading
          begin
            job = pop
            next if job.nil?
            download job
          rescue e
            Logger.error e
            @downloading = false
          end
        end
      end
    end

    abstract def pop : Job?
    private abstract def download(job : Job)
  end

  enum JobStatus
    Pending      # 0
    Downloading  # 1
    Error        # 2
    Completed    # 3
    MissingPages # 4
  end

  struct Job
    property id : String
    property manga_id : String
    property title : String
    property manga_title : String
    property status : JobStatus
    property status_message : String = ""
    property pages : Int32 = 0
    property success_count : Int32 = 0
    property fail_count : Int32 = 0
    property time : Time
    property plugin_id : String?
    property plugin_chapter_id : String?

    def parse_query_result(res : DB::ResultSet)
      @id = res.read String
      @manga_id = res.read String
      @title = res.read String
      @manga_title = res.read String
      status = res.read Int32
      @status_message = res.read String
      @pages = res.read Int32
      @success_count = res.read Int32
      @fail_count = res.read Int32
      time = res.read Int64
      @status = JobStatus.new status
      @time = Time.unix_ms time

      ary = @id.split("-")
      if ary.size == 2
        @plugin_id = ary[0]
        @plugin_chapter_id = ary[1]
      end
    end

    # Raises if the result set does not contain the correct set of columns
    def self.from_query_result(res : DB::ResultSet)
      job = Job.allocate
      job.parse_query_result res
      job
    end

    def initialize(@id, @manga_id, @title, @manga_title, @status, @time,
                   @plugin_id = nil)
    end

    def to_json(json)
      json.object do
        {% for name in ["id", "manga_id", "title", "manga_title",
                        "status_message"] %}
          json.field {{name}}, @{{name.id}}
        {% end %}
        {% for name in ["pages", "success_count", "fail_count"] %}
          json.field {{name}} do
            json.number @{{name.id}}
          end
        {% end %}
        json.field "status", @status.to_s
        json.field "time" do
          json.number @time.to_unix_ms
        end
        json.field "plugin_id", @plugin_id if @plugin_id
      end
    end
  end

  getter path : String
  @downloaders = [] of Downloader
  @paused = false

  use_default

  def initialize(db_path : String? = nil)
    @path = db_path || Config.current.mangadex["download_queue_db_path"].to_s
    dir = File.dirname @path
    unless Dir.exists? dir
      Logger.info "The queue DB directory #{dir} does not exist. " \
                  "Attepmting to create it"
      Dir.mkdir_p dir
    end
    MainFiber.run do
      DB.open "sqlite3://#{@path}" do |db|
        begin
          db.exec "create table if not exists queue " \
                  "(id text, manga_id text, title text, manga_title " \
                  "text, status integer, status_message text, " \
                  "pages integer, success_count integer, " \
                  "fail_count integer, time integer)"
          db.exec "create unique index if not exists id_idx " \
                  "on queue (id)"
          db.exec "create index if not exists manga_id_idx " \
                  "on queue (manga_id)"
          db.exec "create index if not exists status_idx " \
                  "on queue (status)"
        rescue e
          Logger.error "Error when checking tables in DB: #{e}"
          raise e
        end
      end
    end
  end

  # Push an array of jobs into the queue, and return the number of jobs
  #   inserted. Any job already exists in the queue will be ignored.
  def push(jobs : Array(Job))
    start_count = self.count
    MainFiber.run do
      DB.open "sqlite3://#{@path}" do |db|
        jobs.each do |job|
          db.exec "insert or ignore into queue values " \
                  "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
            job.id, job.manga_id, job.title, job.manga_title,
            job.status.to_i, job.status_message, job.pages,
            job.success_count, job.fail_count, job.time.to_unix_ms
        end
      end
    end
    self.count - start_count
  end

  def reset(id : String)
    MainFiber.run do
      DB.open "sqlite3://#{@path}" do |db|
        db.exec "update queue set status = 0, status_message = '', " \
                "pages = 0, success_count = 0, fail_count = 0 " \
                "where id = (?)", id
      end
    end
  end

  def reset(job : Job)
    self.reset job.id
  end

  # Reset all failed tasks (missing pages and error)
  def reset
    MainFiber.run do
      DB.open "sqlite3://#{@path}" do |db|
        db.exec "update queue set status = 0, status_message = '', " \
                "pages = 0, success_count = 0, fail_count = 0 " \
                "where status = 2 or status = 4"
      end
    end
  end

  def delete(id : String)
    MainFiber.run do
      DB.open "sqlite3://#{@path}" do |db|
        db.exec "delete from queue where id = (?)", id
      end
    end
  end

  def delete(job : Job)
    self.delete job.id
  end

  def exists?(id : String)
    res = false
    MainFiber.run do
      DB.open "sqlite3://#{@path}" do |db|
        res = db.query_one "select count(*) from queue where id = (?)", id,
          as: Bool
      end
    end
    res
  end

  def exists?(job : Job)
    self.exists? job.id
  end

  def delete_status(status : JobStatus)
    MainFiber.run do
      DB.open "sqlite3://#{@path}" do |db|
        db.exec "delete from queue where status = (?)", status.to_i
      end
    end
  end

  def count_status(status : JobStatus)
    num = 0
    MainFiber.run do
      DB.open "sqlite3://#{@path}" do |db|
        num = db.query_one "select count(*) from queue where " \
                           "status = (?)", status.to_i, as: Int32
      end
    end
    num
  end

  def count
    num = 0
    MainFiber.run do
      DB.open "sqlite3://#{@path}" do |db|
        num = db.query_one "select count(*) from queue", as: Int32
      end
    end
    num
  end

  def set_status(status : JobStatus, job : Job)
    MainFiber.run do
      DB.open "sqlite3://#{@path}" do |db|
        db.exec "update queue set status = (?) where id = (?)",
          status.to_i, job.id
      end
    end
  end

  def get_all
    jobs = [] of Job
    MainFiber.run do
      DB.open "sqlite3://#{@path}" do |db|
        jobs = db.query_all "select * from queue order by time" do |rs|
          Job.from_query_result rs
        end
      end
    end
    jobs
  end

  def add_success(job : Job)
    MainFiber.run do
      DB.open "sqlite3://#{@path}" do |db|
        db.exec "update queue set success_count = success_count + 1 " \
                "where id = (?)", job.id
      end
    end
  end

  def add_fail(job : Job)
    MainFiber.run do
      DB.open "sqlite3://#{@path}" do |db|
        db.exec "update queue set fail_count = fail_count + 1 " \
                "where id = (?)", job.id
      end
    end
  end

  def set_pages(pages : Int32, job : Job)
    MainFiber.run do
      DB.open "sqlite3://#{@path}" do |db|
        db.exec "update queue set pages = (?), success_count = 0, " \
                "fail_count = 0 where id = (?)", pages, job.id
      end
    end
  end

  def add_message(msg : String, job : Job)
    MainFiber.run do
      DB.open "sqlite3://#{@path}" do |db|
        db.exec "update queue set status_message = " \
                "status_message || (?) || (?) where id = (?)",
          "\n", msg, job.id
      end
    end
  end

  def <<(downloader : Downloader)
    @downloaders << downloader
  end

  def pause
    @downloaders.each { |d| d.stopped = true }
    @paused = true
  end

  def resume
    @downloaders.each { |d| d.stopped = false }
    @paused = false
  end

  def paused?
    @paused
  end
end
