#!/usr/bin/env ruby

require "fileutils"
require "open-uri"
require "net/http"
require "rmagick"
include Magick

TEMP_DIR = "./.ms.tmp"
TILES_DIR = "./tiles"

# False if URL returns a 200
#
# @param url [String] the url to be checked
# @return [Boolean] whether or not the URL is invalid
def invalid?(url)
  uri = URI(url)
  !Net::HTTP.start(uri.host, 443, use_ssl: true){ |http| break http.head uri.path }.instance_of? Net::HTTPOK
end

# Parses ARGV input into usable URL with given X and Y values
#
# @param x [Integer] x-coordinate
# @param y [Integer] y-coordinate
# @return usable URL for map tile
def build_url(url, x, y)
  url.gsub(/xxx/i, x.to_s).gsub(/yyy/i, y.to_s)
end

if ARGV.empty? or %w[-h --help].include? ARGV.first
  puts 'Usage: mapsnatcher "http://mapwebsite.com/0/XXX/YYY.png"'
  puts
  puts "       Place XXX and YYY in place of where the respective coordinate numbers would be"
  puts "       in the URL. The first number is typically the zoom level, which is constant."
  puts
  puts "  [-b] find outer bounds of map automatically"
  puts "  [-s] save tiles to a directory ./tiles/"
  exit 0
end

if %w[-v --version].include? ARGV.first
  puts "mapsnatcher #{File.read(File.expand_path("../../VERSION", __FILE__)).strip}"
  exit 0
end

# Load/create temp directory
if !File.exists? TEMP_DIR
  Dir.mkdir TEMP_DIR
elsif !Dir.empty? TEMP_DIR
  print "It looks like mapsnatcher didn't close properly. Use backed up files? [Y/n] "
  FileUtils.rm_f(Dir.glob("#{TEMP_DIR}/*")) if STDIN.gets.chomp =~ /n.*/i
  puts
end

save_tiles = false
if (args = ARGV).include? "-s"
  args -= ["-s"]
  save_tiles = true
end

if args.include? "-b"
  # Automatic boundary mode

  url = (args - ["-b"]).first

  # CLI input function
  #
  # @return [Array] user input coordinates
  def get_coords
    print "Enter valid X coordinate [0]: "
    x_in = STDIN.gets.chomp.to_i
    print "Enter valid Y coordinate [0]: "
    y_in = STDIN.gets.chomp.to_i

    print "\nLoading...\r"

    [x_in, y_in]
  end

  valid_x, valid_y = get_coords

  while invalid? build_url(url, valid_x, valid_y)
    puts "Invalid coordinate! Try again."
    puts
    valid_x, valid_y = get_coords
  end

  x_low = x_high = valid_x
  until invalid? build_url(url, x_low - 1, valid_y)
    print "Checking X value: #{x_low}     \r"
    x_low -= 1
  end
  until invalid? build_url(url, x_high + 1, valid_y)
    print "Checking X value: #{x_high}     \r"
    x_high += 1
  end

  y_low = y_high = valid_y
  until invalid? build_url(url, valid_x, y_low - 1)
    print "Checking Y value: #{y_low}     \r"
    y_low -= 1
  end
  until invalid? build_url(url, valid_x, y_high + 1)
    print "Checking Y value: #{y_high}     \r"
    y_high += 1
  end

  puts "Found Coordinate Range: [#{x_low}-#{x_high}], [#{y_low}-#{y_high}]"

  x_range, y_range = (x_low..x_high), (y_low..y_high)
else
  # User input coordinate mode

  url = args.first

  # CLI input function
  #
  # @return [Array] user input coordinate ranges
  def get_coords
    print "Start of X range [0]: "
    x_start = STDIN.gets.chomp.to_i
    print "End of X range: "
    x_range_in = (x_start..STDIN.gets.chomp.to_i)

    print "Start of Y range [0]: "
    y_start = STDIN.gets.chomp.to_i
    print "End of Y range: "
    y_range_in = (y_start..STDIN.gets.chomp.to_i)

    print "\nLoading...\r"

    [x_range_in, y_range_in]
  end

  x_range, y_range = get_coords

  while invalid?(build_url url, x_range.first, y_range.first) or
        invalid?(build_url url, x_range.first, y_range.last)  or
        invalid?(build_url url, x_range.last, y_range.first)  or
        invalid?(build_url url, x_range.last, y_range.last)
    puts "\nError: Boundaries incorrect."
    puts "Use `mapsnatcher -b` for help finding the boundaries of the image."
    puts
    x_range, y_range = get_coords
  end
end

# Use the tile at 0, 0 to calculate final image size
sample = Image.from_blob(URI.open(build_url url, x_range.first, y_range.first).read).first
final_size = {x: sample.columns * x_range.size, y: sample.rows * y_range.size}
format = sample.format
puts "Image found, #{format} size #{final_size[:x]}x#{final_size[:y]}"
puts

dl_counter = 0
dl_total = x_range.size * y_range.size

stitch = ImageList.new
y_range.each do |y|
  row = ImageList.new
  x_range.each do |x|
    print "Downloading... [#{dl_counter += 1}/#{dl_total}]\r"

    temp = "#{TEMP_DIR}/#{x}_#{y}.#{format.downcase}"
    if File.exists? temp
      # Load the backup file
      img = Image.read(temp).first
    else
      blob = URI.open(build_url url, x, y).read
      File.open(temp, "wb") { |f| f.write blob }
      img = Image.from_blob(blob).first
    end

    row.push img
  end

  stitch.push row.append(false)
end

puts "\nDone!"

# Warnings for incompatible formats for large image sizes
JPEG_MAX = 65535
WEBP_MAX = 16383

if final_size.values.max > JPEG_MAX
  puts "\nWarning! Image dimensions greater than #{JPEG_MAX}px, JPG and WEBP"
  puts "formats are unavailable."
  puts
elsif final_size.values.max > WEBP_MAX
  puts "\nWarning! Image dimensions greater than #{WEBP_MAX}px, WEBP"
  puts "format is unavailable."
  puts
end

print "Enter file name for map: "
filename = STDIN.gets.chomp
print "Saving, this may take a minute..."
stitch.append(true).write filename
puts "\nImage written to: #{filename}"

if save_tiles
  File.rename TEMP_DIR, TILES_DIR
  puts "Tiles saved to: #{TILES_DIR.match /\w+/}/"
else
  FileUtils.rm_rf TEMP_DIR
end