#!/usr/bin/env python3

"""Find all US zip codes within n miles of a central zip code, reporting the zipcode, city, state and (center) distance."""

import csv
import functools
import math
# import pprint
import sys


@functools.total_ordering
class Zipcode:
    """Hold one zipcode."""

    def __init__(self, *, zipno, city, state, latitude, longitude):
        """Initialize."""
        # zipno is a string, because some zipcodes start with a leading zero, and we don't want to lose that
        self.zipno = zipno
        self.city = city
        self.state = state
        self.latitude = latitude
        self.longitude = longitude

    def __str__(self):
        """Convert to a string."""
        return f'{self.zipno}, {self.city}, {self.state}'

    def __repr__(self):
        """Convert to a string for debugging."""
        return str(self) + f', {self.latitude}, {self.longitude}'

    def __eq__(self, other):
        """Compare latitude, and longitude - for the sake of sorting."""
        return self.latitude == other.latitude and self.longitude == other.longitude

    def __lt__(self, other):
        """Compare latitude and longitude - for the sake of sorting."""
        if self.latitude < other.latitude:
            return True
        if self.longitude < other.longitude:
            return True
        return False

    # based on "improved accuracy" at http://www.meridianworlddata.com/distance-calculation.asp
    def approximate_distance_in_miles(self, other):
        """Approximate the distance in miles, from one lat/long to another."""
        scale = 69.1
        x = scale * (other.latitude - self.latitude)
        y = scale * (other.longitude - self.longitude) * math.cos(self.latitude / 57.3)
        return (x ** 2 + y ** 2) ** 0.5


def gen_zipcodes_1():
    """Generate zipcodes from a .csv file."""
    # This came from https://public.opendatasoft.com/explore/dataset/us-zip-code-latitude-and-longitude/table/
    # I suspect the data may be off, because Santa Clarita came up within 40 miles of Laguna Niguel, but it should be more like
    # 97 miles according to Google Maps. Granted, Google Maps is not as the crow flies, but it still shouldn't differ by that
    # much.
    with open('us-zip-code-latitude-and-longitude.csv', newline='') as csvfile:
        csv_reader = csv.reader(csvfile, delimiter=';', quotechar='"')
        for row in csv_reader:
            if row[0] == 'Zip':
                continue
            # zipno, city, state, latitude, longitude
            zipcode = Zipcode(zipno=row[0], city=row[1], state=row[2], latitude=row[3], longitude=row[4])
            yield zipcode


def gen_zipcodes_2():
    """Generate zipcodes from a .csv file."""
    # This came from https://simplemaps.com/data/us-zips
    with open('uszips.csv', newline='') as csvfile:
        csv_reader = csv.reader(csvfile, delimiter=',', quotechar='"')
        for row in csv_reader:
            if row[0] == 'zip':
                continue
            # zipno, city, state, latitude, longitude
            zipcode = Zipcode(zipno=row[0], city=row[3], state=row[4], latitude=float(row[1]), longitude=float(row[2]))
            yield zipcode


def main():
    """Start the ball rolling."""
    center_zipno = None
    max_distance = None

    while sys.argv[1:]:
        if sys.argv[1] == '--center-zip':
            # center_zip is a string, because some zipcodes start with a leading zero, and we don't want to lose that
            center_zipno = sys.argv[2]
            del sys.argv[1]
        elif sys.argv[1] == '--max-distance':
            max_distance = float(sys.argv[2])
        del sys.argv[1]

    all_good = True

    if center_zipno is None:
        print('--center-zip is a required option', file=sys.stderr)
        all_good = False

    if max_distance is None:
        print('--max-distance is a required option', file=sys.stderr)
        all_good = False

    if not all_good:
        print('One or more items in preflight check failed', file=sys.stderr)
        sys.exit(1)

    zipcodes = list(gen_zipcodes_2())

    zipcodes.sort()

    zipcodes_by_zip = {zipcode.zipno: zipcode for zipcode in zipcodes}

    # pprint.pprint(zipcodes)
    # pprint.pprint(zipcodes_by_zip)

    center_zipcode = zipcodes_by_zip[center_zipno]

    result_zipcodes = []
    for zipcode in zipcodes:
        approx_distance = center_zipcode.approximate_distance_in_miles(zipcode)
        if approx_distance <= max_distance:
            tuple_ = (approx_distance, zipcode)
            result_zipcodes.append(tuple_)

    result_zipcodes.sort()

    print('Distance in miles, Zipcode, City, State')
    for approx_distance, result_zipcode in result_zipcodes:
        print(f'{approx_distance:.1f}, {repr(result_zipcode)}')


main()