//
//  LocationManager.swift
//
//
//  Created by Jimmy Jose on 14/08/14.
//
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.


import UIKit
import CoreLocation
import MapKit


typealias LMReverseGeocodeCompletionHandler = ((reverseGecodeInfo:NSDictionary?,placemark:CLPlacemark?, error:String?)->Void)?
typealias LMGeocodeCompletionHandler = ((gecodeInfo:NSDictionary?,placemark:CLPlacemark?, error:String?)->Void)?
typealias LMLocationCompletionHandler = ((latitude:Double, longitude:Double, status:String, verboseMessage:String, error:String?)->())?

// Todo: Keep completion handler differerent for all services, otherwise only one will work
enum GeoCodingType{
  
  case Geocoding
  case ReverseGeocoding
}

class LocationManager: NSObject,CLLocationManagerDelegate {
  
  /* Private variables */
  private var completionHandler:LMLocationCompletionHandler
  
  private var reverseGeocodingCompletionHandler:LMReverseGeocodeCompletionHandler
  private var geocodingCompletionHandler:LMGeocodeCompletionHandler
  
  private var locationStatus = NSLocalizedString("Calibrating", comment: "")// to pass in handler
  private var locationManager: CLLocationManager!
  private var verboseMessage = NSLocalizedString("Calibrating", comment: "")
  
  private let verboseMessageDictionary = [CLAuthorizationStatus.NotDetermined:NSLocalizedString("You have not yet made a choice with regards to this application.", comment: ""),
    CLAuthorizationStatus.Restricted:NSLocalizedString("This application is not authorized to use location services. Due to active restrictions on location services, the user cannot change this status, and may not have personally denied authorization.", comment: ""),
    CLAuthorizationStatus.Denied:NSLocalizedString("You have explicitly denied authorization for this application, or location services are disabled in Settings.", comment: ""),
    CLAuthorizationStatus.AuthorizedAlways:NSLocalizedString("App is Authorized to always use location services.", comment: ""),CLAuthorizationStatus.AuthorizedWhenInUse:NSLocalizedString("You have granted authorization to use your location only when the app is visible to you.", comment: "")]
  
  
  var delegate:LocationManagerDelegate? = nil
  
  var latitude:Double = 0.0
  var longitude:Double = 0.0
  
  var latitudeAsString:String = ""
  var longitudeAsString:String = ""
  
  
  var lastKnownLatitude:Double = 0.0
  var lastKnownLongitude:Double = 0.0
  
  var lastKnownLatitudeAsString:String = ""
  var lastKnownLongitudeAsString:String = ""
  
  
  var keepLastKnownLocation:Bool = true
  var hasLastKnownLocation:Bool = true
  
  var autoUpdate:Bool = false
  
  var showVerboseMessage = false
  
  var isRunning = false
  
  
  class var sharedInstance : LocationManager {
    struct Static {
      static let instance : LocationManager = LocationManager()
    }
    return Static.instance
  }
  
  
  private override init(){
    
    super.init()
    
    if !autoUpdate {
      autoUpdate = !CLLocationManager.significantLocationChangeMonitoringAvailable()
    }
  }
  
  private func resetLatLon(){
    
    latitude = 0.0
    longitude = 0.0
    
    latitudeAsString = ""
    longitudeAsString = ""
  }
  
  private func resetLastKnownLatLon(){
    
    hasLastKnownLocation = false
    
    lastKnownLatitude = 0.0
    lastKnownLongitude = 0.0
    
    lastKnownLatitudeAsString = ""
    lastKnownLongitudeAsString = ""
  }
  
  func startUpdatingLocationWithCompletionHandler(completionHandler:((latitude:Double, longitude:Double, status:String, verboseMessage:String, error:String?)->())? = nil){
    
    self.completionHandler = completionHandler
    
    initLocationManager()
  }
  
  
  func startUpdatingLocation(){
    
    initLocationManager()
  }
  
  func stopUpdatingLocation(){
    
    if autoUpdate {
      locationManager.stopUpdatingLocation()
    } else {
      locationManager.stopMonitoringSignificantLocationChanges()
    }
    
    resetLatLon()
    if !keepLastKnownLocation {
      resetLastKnownLatLon()
    }
  }
  
  private func initLocationManager() {
    
    // App might be unreliable if someone changes autoupdate status in between and stops it
    
    locationManager = CLLocationManager()
    locationManager.delegate = self
    // locationManager.locationServicesEnabled
    locationManager.desiredAccuracy = kCLLocationAccuracyBest
    
    if NSString(string: UIDevice.currentDevice().systemVersion).doubleValue >= 8 {
      
      //locationManager.requestAlwaysAuthorization() // add in plist NSLocationAlwaysUsageDescription
      locationManager.requestWhenInUseAuthorization() // add in plist NSLocationWhenInUseUsageDescription
    }
    
    startLocationManger()
  }
  
  private func startLocationManger() {
    
    if autoUpdate {
      
      locationManager.startUpdatingLocation()
    } else {
      
      locationManager.startMonitoringSignificantLocationChanges()
    }
    
    isRunning = true
  }
  
  private func stopLocationManger() {
    
    if autoUpdate {
      
      locationManager.stopUpdatingLocation()
    } else {
      
      locationManager.stopMonitoringSignificantLocationChanges()
    }
    
    isRunning = false
  }
  
  internal func locationManager(manager: CLLocationManager!, didFailWithError error: NSError!) {
    
    stopLocationManger()
    
    resetLatLon()
    
    if !keepLastKnownLocation {
      
      resetLastKnownLatLon()
    }
    
    var verbose = ""
    
    if showVerboseMessage {verbose = verboseMessage}
    completionHandler?(latitude: 0.0, longitude: 0.0, status: locationStatus, verboseMessage:verbose,error: error.localizedDescription)
    
    if (delegate != nil) && (delegate?.respondsToSelector(Selector("locationManagerReceivedError:")))! {
      delegate?.locationManagerReceivedError!(error.localizedDescription)
    }
  }
  
  internal func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {
    
    let arrayOfLocation = locations as NSArray
    let location = arrayOfLocation.lastObject as! CLLocation
    let coordLatLon = location.coordinate
    
    latitude  = coordLatLon.latitude
    longitude = coordLatLon.longitude
    
    latitudeAsString  = coordLatLon.latitude.description
    longitudeAsString = coordLatLon.longitude.description
    
    var verbose = ""
    
    if showVerboseMessage {verbose = verboseMessage}
    
    if completionHandler != nil {
      
      completionHandler?(latitude: latitude, longitude: longitude, status: locationStatus, verboseMessage:verbose, error: nil)
    }
    
    lastKnownLatitude = coordLatLon.latitude
    lastKnownLongitude = coordLatLon.longitude
    
    lastKnownLatitudeAsString = coordLatLon.latitude.description
    lastKnownLongitudeAsString = coordLatLon.longitude.description
    
    hasLastKnownLocation = true
    
    if delegate != nil {
      if (delegate?.respondsToSelector(Selector("locationFoundGetAsString:longitude:")))! {
        delegate?.locationFoundGetAsString!(latitudeAsString,longitude:longitudeAsString)
      }
      if (delegate?.respondsToSelector(Selector("locationFound:longitude:")))! {
        delegate?.locationFound(latitude,longitude:longitude)
      }
    }
  }
  
  internal func locationManager(manager: CLLocationManager!,
    didChangeAuthorizationStatus status: CLAuthorizationStatus) {
      var hasAuthorised = false
      var verboseKey = status
      switch status {
      case CLAuthorizationStatus.Restricted:
        locationStatus = NSLocalizedString("Restricted Access", comment: "")
      case CLAuthorizationStatus.Denied:
        locationStatus = NSLocalizedString("Denied access", comment: "")
      case CLAuthorizationStatus.NotDetermined:
        locationStatus = NSLocalizedString("Not determined", comment: "")
      default:
        locationStatus = NSLocalizedString("Allowed access", comment: "")
        hasAuthorised = true
      }
      
      verboseMessage = verboseMessageDictionary[verboseKey]!
      
      if hasAuthorised {
        startLocationManger()
      } else {
        
        resetLatLon()
        if !(locationStatus == NSLocalizedString("Denied access", comment: "")) {
          
          var verbose = ""
          if showVerboseMessage {
            
            verbose = verboseMessage
            
            if (delegate != nil) && (delegate?.respondsToSelector(Selector("locationManagerVerboseMessage:")))! {
              
              delegate?.locationManagerVerboseMessage!(verbose)
            }
          }
          
          if completionHandler != nil {
            completionHandler?(latitude: latitude, longitude: longitude, status: locationStatus, verboseMessage:verbose,error: nil)
          }
        }
        if (delegate != nil) && (delegate?.respondsToSelector(Selector("locationManagerStatus:")))! {
          delegate?.locationManagerStatus!(locationStatus)
        }
      }
  }
  
  func reverseGeocodeLocationWithLatLon(#latitude:Double, longitude: Double,onReverseGeocodingCompletionHandler:LMReverseGeocodeCompletionHandler){
    
    let location:CLLocation = CLLocation(latitude:latitude, longitude: longitude)
    
    reverseGeocodeLocationWithCoordinates(location, onReverseGeocodingCompletionHandler: onReverseGeocodingCompletionHandler)
  }
  
  func reverseGeocodeLocationWithCoordinates(coord:CLLocation, onReverseGeocodingCompletionHandler:LMReverseGeocodeCompletionHandler){
    
    self.reverseGeocodingCompletionHandler = onReverseGeocodingCompletionHandler
    
    reverseGocode(coord)
  }
  
  private func reverseGocode(location:CLLocation){
    
    let geocoder: CLGeocoder = CLGeocoder()
    
    geocoder.reverseGeocodeLocation(location, completionHandler: {(placemarks, error)->Void in
      
      if error != nil {
        self.reverseGeocodingCompletionHandler!(reverseGecodeInfo:nil,placemark:nil, error: error.localizedDescription)
      } else {
        
        if let placemark = placemarks?[0] as? CLPlacemark {
          var address = AddressParser()
          address.parseAppleLocationData(placemark)
          let addressDict = address.getAddressDictionary()
          self.reverseGeocodingCompletionHandler!(reverseGecodeInfo: addressDict,placemark:placemark,error: nil)
        } else {
          self.reverseGeocodingCompletionHandler!(reverseGecodeInfo: nil,placemark:nil,error: NSLocalizedString("No Placemarks Found!", comment: ""))
          return
        }
      }
    })
  }
  
  func geocodeAddressString(#address:NSString, onGeocodingCompletionHandler:LMGeocodeCompletionHandler){
    
    self.geocodingCompletionHandler = onGeocodingCompletionHandler
    
    geoCodeAddress(address)
  }
  
  private func geoCodeAddress(address:NSString){
    
    let geocoder = CLGeocoder()
    geocoder.geocodeAddressString(address as String, completionHandler: {(placemarks: [AnyObject]!, error: NSError!) -> Void in
      
      if error != nil {
        
        self.geocodingCompletionHandler!(gecodeInfo:nil,placemark:nil,error: error.localizedDescription)
      } else {
        if let placemark = placemarks?[0] as? CLPlacemark {
          var address = AddressParser()
          address.parseAppleLocationData(placemark)
          let addressDict = address.getAddressDictionary()
          self.geocodingCompletionHandler!(gecodeInfo: addressDict,placemark:placemark,error: nil)
        } else {
          self.geocodingCompletionHandler!(gecodeInfo: nil,placemark:nil,error: NSLocalizedString("invalid address: \(address)", comment: ""))
        }
      }
    })
  }
  
  func geocodeUsingGoogleAddressString(#address:NSString, onGeocodingCompletionHandler:LMGeocodeCompletionHandler){
    
    self.geocodingCompletionHandler = onGeocodingCompletionHandler
    
    geoCodeUsignGoogleAddress(address)
  }
  
  private func geoCodeUsignGoogleAddress(address:NSString){
    
    var urlString = "http://maps.googleapis.com/maps/api/geocode/json?address=\(address)" as NSString
    
    urlString = urlString.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding)!
    
    performOperationForURL(urlString, type: GeoCodingType.Geocoding)
  }
  
  func reverseGeocodeLocationUsingGoogleWithLatLon(#latitude:Double, longitude: Double,onReverseGeocodingCompletionHandler:LMReverseGeocodeCompletionHandler){
    
    self.reverseGeocodingCompletionHandler = onReverseGeocodingCompletionHandler
    
    reverseGocodeUsingGoogle(latitude: latitude, longitude: longitude)
  }
  
  func reverseGeocodeLocationUsingGoogleWithCoordinates(coord:CLLocation, onReverseGeocodingCompletionHandler:LMReverseGeocodeCompletionHandler){
    
    reverseGeocodeLocationUsingGoogleWithLatLon(latitude: coord.coordinate.latitude, longitude: coord.coordinate.longitude, onReverseGeocodingCompletionHandler: onReverseGeocodingCompletionHandler)
  }
  
  private func reverseGocodeUsingGoogle(#latitude:Double, longitude: Double){
    
    var urlString = "http://maps.googleapis.com/maps/api/geocode/json?latlng=\(latitude),\(longitude)" as NSString
    
    urlString = urlString.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding)!
    
    performOperationForURL(urlString, type: GeoCodingType.ReverseGeocoding)
  }
  
  private func performOperationForURL(urlString:NSString,type:GeoCodingType){
    
    let url:NSURL? = NSURL(string:urlString as String)
    
    let request:NSURLRequest = NSURLRequest(URL:url!)
    
    let queue:NSOperationQueue = NSOperationQueue()
    
    NSURLConnection.sendAsynchronousRequest(request,queue:queue,completionHandler:{response,data,error in
      
      if error != nil {
        
        self.setCompletionHandler(responseInfo:nil, placemark:nil, error:error.localizedDescription, type:type)
      } else {
        
        let kStatus = "status"
        let kOK = "ok"
        let kZeroResults = "ZERO_RESULTS"
        let kAPILimit = "OVER_QUERY_LIMIT"
        let kRequestDenied = "REQUEST_DENIED"
        let kInvalidRequest = "INVALID_REQUEST"
        let kInvalidInput =  "Invalid Input"
        
        let dataAsString: NSString? = NSString(data: data, encoding: NSUTF8StringEncoding)
        
        let jsonResult: NSDictionary = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: nil) as! NSDictionary
        
        var status = jsonResult.valueForKey(kStatus) as! NSString
        status = status.lowercaseString
        
        if status.isEqualToString(kOK) {
          
          let address = AddressParser()
          
          address.parseGoogleLocationData(jsonResult)
          
          let addressDict = address.getAddressDictionary()
          let placemark:CLPlacemark = address.getPlacemark()
          
          self.setCompletionHandler(responseInfo:addressDict, placemark:placemark, error: nil, type:type)
          
        } else if !status.isEqualToString(kZeroResults) && !status.isEqualToString(kAPILimit) && !status.isEqualToString(kRequestDenied) && !status.isEqualToString(kInvalidRequest) {
          
          self.setCompletionHandler(responseInfo:nil, placemark:nil, error:kInvalidInput, type:type)
        } else {
          
          //status = (status.componentsSeparatedByString("_") as NSArray).componentsJoinedByString(" ").capitalizedString
          self.setCompletionHandler(responseInfo:nil, placemark:nil, error:status as String, type:type)
        }
      }
    })
  }
  
  private func setCompletionHandler(#responseInfo:NSDictionary?,placemark:CLPlacemark?, error:String?,type:GeoCodingType){
    
    if type == GeoCodingType.Geocoding {
      self.geocodingCompletionHandler!(gecodeInfo:responseInfo,placemark:placemark,error:error)
    } else {
      self.reverseGeocodingCompletionHandler!(reverseGecodeInfo:responseInfo,placemark:placemark,error:error)
    }
  }
}

@objc protocol LocationManagerDelegate : NSObjectProtocol
{
  func locationFound(latitude:Double, longitude:Double)
  optional func locationFoundGetAsString(latitude:NSString, longitude:NSString)
  optional func locationManagerStatus(status:NSString)
  optional func locationManagerReceivedError(error:NSString)
  optional func locationManagerVerboseMessage(message:NSString)
}

private class AddressParser: NSObject{
  
  private var latitude = NSString()
  private var longitude  = NSString()
  private var streetNumber = NSString()
  private var route = NSString()
  private var locality = NSString()
  private var subLocality = NSString()
  private var formattedAddress = NSString()
  private var administrativeArea = NSString()
  private var administrativeAreaCode = NSString()
  private var subAdministrativeArea = NSString()
  private var postalCode = NSString()
  private var country = NSString()
  private var subThoroughfare = NSString()
  private var thoroughfare = NSString()
  private var ISOcountryCode = NSString()
  private var state = NSString()
  
  
  override init() {
    
    super.init()
  }
  
  private func getAddressDictionary()-> NSDictionary {
    
    var addressDict = NSMutableDictionary()
    
    addressDict.setValue(latitude, forKey: "latitude")
    addressDict.setValue(longitude, forKey: "longitude")
    addressDict.setValue(streetNumber, forKey: "streetNumber")
    addressDict.setValue(locality, forKey: "locality")
    addressDict.setValue(subLocality, forKey: "subLocality")
    addressDict.setValue(administrativeArea, forKey: "administrativeArea")
    addressDict.setValue(postalCode, forKey: "postalCode")
    addressDict.setValue(country, forKey: "country")
    addressDict.setValue(formattedAddress, forKey: "formattedAddress")
    
    return addressDict
  }
  
  private func parseAppleLocationData(placemark:CLPlacemark) {
    
    var addressLines = placemark.addressDictionary["FormattedAddressLines"] as! NSArray
    
    //self.streetNumber = placemark.subThoroughfare ? placemark.subThoroughfare : ""
    self.streetNumber = placemark.thoroughfare != nil ? placemark.thoroughfare : ""
    self.locality = placemark.locality != nil ? placemark.locality : ""
    self.postalCode = placemark.postalCode != nil ? placemark.postalCode : ""
    self.subLocality = placemark.subLocality != nil ? placemark.subLocality : ""
    self.administrativeArea = placemark.administrativeArea != nil ? placemark.administrativeArea : ""
    self.country = placemark.country != nil ?  placemark.country : ""
    self.longitude = placemark.location.coordinate.longitude.description;
    self.latitude = placemark.location.coordinate.latitude.description
    if addressLines.count>0 {
      self.formattedAddress = addressLines.componentsJoinedByString(", ")
    } else {
      self.formattedAddress = ""
    }
  }
  
  private func parseGoogleLocationData(resultDict:NSDictionary) {
    
    let locationDict = (resultDict.valueForKey("results") as! NSArray).firstObject as! NSDictionary
    
    let formattedAddrs = locationDict.objectForKey("formatted_address") as! NSString
    
    let geometry = locationDict.objectForKey("geometry") as! NSDictionary
    let location = geometry.objectForKey("location") as! NSDictionary
    let lat = location.objectForKey("lat") as! Double
    let lng = location.objectForKey("lng") as! Double
    
    self.latitude = lat.description
    self.longitude = lng.description
    
    let addressComponents = locationDict.objectForKey("address_components") as! NSArray
    
    self.subThoroughfare = component("street_number", inArray: addressComponents, ofType: "long_name")
    self.thoroughfare = component("route", inArray: addressComponents, ofType: "long_name")
    self.streetNumber = self.subThoroughfare
    self.locality = component("locality", inArray: addressComponents, ofType: "long_name")
    self.postalCode = component("postal_code", inArray: addressComponents, ofType: "long_name")
    self.route = component("route", inArray: addressComponents, ofType: "long_name")
    self.subLocality = component("subLocality", inArray: addressComponents, ofType: "long_name")
    self.administrativeArea = component("administrative_area_level_1", inArray: addressComponents, ofType: "long_name")
    self.administrativeAreaCode = component("administrative_area_level_1", inArray: addressComponents, ofType: "short_name")
    self.subAdministrativeArea = component("administrative_area_level_2", inArray: addressComponents, ofType: "long_name")
    self.country =  component("country", inArray: addressComponents, ofType: "long_name")
    self.ISOcountryCode =  component("country", inArray: addressComponents, ofType: "short_name")
    self.formattedAddress = formattedAddrs;
  }
  
  private func component(component:NSString,inArray:NSArray,ofType:NSString) -> NSString {
    
    let index:NSInteger = inArray.indexOfObjectPassingTest { (obj, idx, stop) -> Bool in
      
      var objDict:NSDictionary = obj as! NSDictionary
      var types:NSArray = objDict.objectForKey("types") as! NSArray
      let type = types.firstObject as! NSString
      return type.isEqualToString(component as String)
    }
    
    if index == NSNotFound {
      return ""
    }
    
    if index >= inArray.count {
      return ""
    }
    
    var type = ((inArray.objectAtIndex(index) as! NSDictionary).valueForKey(ofType as String)!) as! NSString
    
    if type.length > 0 {
      
      return type
    }
    return ""
  }
  
  private func getPlacemark() -> CLPlacemark {
    
    var addressDict = NSMutableDictionary()
    
    var formattedAddressArray = self.formattedAddress.componentsSeparatedByString(", ") as Array
    
    let kSubAdministrativeArea = "SubAdministrativeArea"
    let kSubLocality           = "SubLocality"
    let kState                 = "State"
    let kStreet                = "Street"
    let kThoroughfare          = "Thoroughfare"
    let kFormattedAddressLines = "FormattedAddressLines"
    let kSubThoroughfare       = "SubThoroughfare"
    let kPostCodeExtension     = "PostCodeExtension"
    let kCity                  = "City"
    let kZIP                   = "ZIP"
    let kCountry               = "Country"
    let kCountryCode           = "CountryCode"
    
    addressDict.setObject(self.subAdministrativeArea, forKey: kSubAdministrativeArea)
    addressDict.setObject(self.subLocality, forKey: kSubLocality)
    addressDict.setObject(self.administrativeAreaCode, forKey: kState)
    
    addressDict.setObject(formattedAddressArray.first as! NSString, forKey: kStreet)
    addressDict.setObject(self.thoroughfare, forKey: kThoroughfare)
    addressDict.setObject(formattedAddressArray, forKey: kFormattedAddressLines)
    addressDict.setObject(self.subThoroughfare, forKey: kSubThoroughfare)
    addressDict.setObject("", forKey: kPostCodeExtension)
    addressDict.setObject(self.locality, forKey: kCity)
    
    
    addressDict.setObject(self.postalCode, forKey: kZIP)
    addressDict.setObject(self.country, forKey: kCountry)
    addressDict.setObject(self.ISOcountryCode, forKey: kCountryCode)
    
    
    var lat = self.latitude.doubleValue
    var lng = self.longitude.doubleValue
    var coordinate = CLLocationCoordinate2D(latitude: lat, longitude: lng)
    
    var placemark = MKPlacemark(coordinate: coordinate, addressDictionary: addressDict as [NSObject : AnyObject])
    
    return (placemark as CLPlacemark)
  }
}
