IMGS_PER_PAGE            = 5
ENTRIES_IN_HOME_SECTIONS = 8
UPLOAD_URL_PREFIX        = "/uploads"
STATIC_DIRS              = %w(/css /js /img /webfonts /favicon.ico /robots.txt)
SUPPORTED_FILE_EXTNAMES  = [".zip", ".cbz", ".rar", ".cbr"]
SUPPORTED_IMG_TYPES      = %w(
  image/jpeg
  image/png
  image/webp
  image/apng
  image/avif
  image/gif
  image/svg+xml
)

def random_str
  UUID.random.to_s.gsub "-", ""
end

# Works in all Unix systems. Follows https://github.com/crystal-lang/crystal/
#   blob/master/src/crystal/system/unix/file_info.cr#L42-L48
def ctime(file_path : String) : Time
  res = LibC.stat(file_path, out stat)
  raise "Unable to get ctime of file #{file_path}" if res != 0

  {% if flag?(:darwin) %}
    Time.new stat.st_ctimespec, Time::Location::UTC
  {% else %}
    Time.new stat.st_ctim, Time::Location::UTC
  {% end %}
end

def register_mime_types
  {
    # Comic Archives
    ".zip" => "application/zip",
    ".rar" => "application/x-rar-compressed",
    ".cbz" => "application/vnd.comicbook+zip",
    ".cbr" => "application/vnd.comicbook-rar",

    # Favicon
    ".ico" => "image/x-icon",

    # FontAwesome fonts
    ".woff"  => "font/woff",
    ".woff2" => "font/woff2",

    # Supported image formats. JPG, PNG, GIF, WebP, and SVG are already
    #   defiend by Crystal in `MIME.DEFAULT_TYPES`
    ".apng" => "image/apng",
    ".avif" => "image/avif",
  }.each do |k, v|
    MIME.register k, v
  end
end

def is_supported_file(path)
  SUPPORTED_FILE_EXTNAMES.includes? File.extname(path).downcase
end

def is_supported_image_file(path)
  SUPPORTED_IMG_TYPES.includes? MIME.from_filename? path
end

struct Int
  def or(other : Int)
    if self == 0
      other
    else
      self
    end
  end
end

struct Nil
  def or(other : Int)
    other
  end
end

macro use_default
  def self.default : self
    unless @@default
      @@default = new
    end
    @@default.not_nil!
  end
end

class String
  def alphanumeric_underscore?
    self.chars.all? { |c| c.alphanumeric? || c == '_' }
  end
end

def env_is_true?(key : String) : Bool
  val = ENV[key.upcase]? || ENV[key.downcase]?
  return false unless val
  val.downcase.in? "1", "true"
end

def sort_titles(titles : Array(Title), opt : SortOptions, username : String)
  cache_key = SortedTitlesCacheEntry.gen_key username, titles, opt
  cached_titles = LRUCache.get cache_key
  return cached_titles if cached_titles.is_a? Array(Title)

  case opt.method
  when .time_modified?
    ary = titles.sort { |a, b| (a.mtime <=> b.mtime).or \
      compare_numerically a.sort_title, b.sort_title }
  when .progress?
    ary = titles.sort do |a, b|
      (a.load_percentage(username) <=> b.load_percentage(username)).or \
        compare_numerically a.sort_title, b.sort_title
    end
  when .title?
    ary = titles.sort do |a, b|
      compare_numerically a.sort_title, b.sort_title
    end
  else
    unless opt.method.auto?
      Logger.warn "Unknown sorting method #{opt.not_nil!.method}. Using " \
                  "Auto instead"
    end
    ary = titles.sort { |a, b| compare_numerically a.sort_title, b.sort_title }
  end

  ary.reverse! unless opt.not_nil!.ascend

  LRUCache.set generate_cache_entry cache_key, ary
  ary
end

def remove_sorted_titles_cache(titles : Array(Title),
                               sort_methods : Array(SortMethod),
                               username : String)
  [false, true].each do |ascend|
    sort_methods.each do |sort_method|
      sorted_titles_cache_key = SortedTitlesCacheEntry.gen_key username,
        titles, SortOptions.new(sort_method, ascend)
      LRUCache.invalidate sorted_titles_cache_key
    end
  end
end

class String
  # Returns the similarity (in [0, 1]) of two paths.
  # For the two paths, separate them into arrays of components, count the
  #   number of matching components backwards, and divide the count by the
  #   number of components of the shorter path.
  def components_similarity(other : String) : Float64
    s, l = [self, other]
      .map { |str| Path.new(str).parts }
      .sort_by! &.size

    match = s.reverse.zip(l.reverse).count { |a, b| a == b }
    match / s.size
  end
end

# Does the followings:
#   - turns space-like characters into the normal whitespaces ( )
#   - strips and collapses spaces
#   - removes ASCII control characters
#   - replaces slashes (/) with underscores (_)
#   - removes leading dots (.)
#   - removes the following special characters: \:*?"<>|
#
# If the sanitized string is empty, returns a random string instead.
def sanitize_filename(str : String) : String
  sanitized = str
    .gsub(/\s+/, " ")
    .strip
    .gsub(/\//, "_")
    .gsub(/^[\.\s]+/, "")
    .gsub(/[\177\000-\031\\:\*\?\"<>\|]/, "")
  sanitized.size > 0 ? sanitized : random_str
end

def delete_cache_and_exit(path : String)
  File.delete path
  Logger.fatal "Invalid library cache deleted. Mango needs to " \
               "perform a full reset to recover from this. " \
               "Pleae restart Mango. This is NOT a bug."
  Logger.fatal "Exiting"
  exit 1
end
