/**
 *  User Lock Manager v4.1.5
 *
 *  Copyright 2015 Erik Thayer
 *  Keypad support added by BLebson
 *  Door manual unlock notifications and option by DimitriRodis 2016-06-24
 *
 *
 */
definition(
  name: "User Lock Manager",
  namespace: "ethayer",
  author: "Erik Thayer",
  description: "This app allows you to change, delete, and schedule user access.",
  category: "Safety & Security",
  iconUrl: "https://dl.dropboxusercontent.com/u/54190708/LockManager/lockmanager.png",
  iconX2Url: "https://dl.dropboxusercontent.com/u/54190708/LockManager/lockmanagerx2.png",
  iconX3Url: "https://dl.dropboxusercontent.com/u/54190708/LockManager/lockmanagerx3.png")

  import groovy.json.JsonSlurper
  import groovy.json.JsonBuilder

preferences {
  page(name: "rootPage")
  page(name: "setupPage")
  page(name: "userPage")
  page(name: "notificationPage")
  page(name: "onUnlockPage")
  page(name: "schedulingPage")
  page(name: "calendarPage")
  page(name: "resetAllCodeUsagePage")
  page(name: "resetCodeUsagePage")
  page(name: "reEnableUserPage")
  page(name: "infoPage")
  page(name: "keypadPage")
  page(name: "infoRefreshPage")
  page(name: "lockInfoPage")
}

def rootPage() {
  //reset errors on each load
  dynamicPage(name: "rootPage", title: "", install: true, uninstall: true) {

    section("Which Locks?") {
      input "theLocks","capability.lockCodes", title: "Select Locks", required: true, multiple: true, submitOnChange: true
    }

    if (theLocks) {
      initalizeLockData()

      section {
        input name: "maxUsers", title: "Number of users", type: "number", multiple: false, refreshAfterSelection: true, submitOnChange: true
        href(name: "toSetupPage", title: "User Settings", page: "setupPage", description: setupPageDescription(), state: setupPageDescription() ? "complete" : "")
        href(name: "toInfoPage", page: "infoPage", title: "Lock Info")
        href(name: "toKeypadPage", page: "keypadPage", title: "Keypad Info (optional)")
        href(name: "toNotificationPage", page: "notificationPage", title: "Notification Settings", description: notificationPageDescription(), state: notificationPageDescription() ? "complete" : "")
        href(name: "toSchedulingPage", page: "schedulingPage", title: "Schedule (optional)", description: schedulingHrefDescription(), state: schedulingHrefDescription() ? "complete" : "")
        href(name: "toOnUnlockPage", page: "onUnlockPage", title: "Global Hello Home")
      }
      section {
        label(title: "Label this SmartApp", required: false, defaultValue: "")
      }
    }
  }
}

def setupPage() {
  dynamicPage(name:"setupPage", title:"User Settings") {
    if (maxUsers > 0) {
      section('Users') {
        (1..maxUsers).each { user->
          if (!state."userState${user}") {
            //there's no values, so reset
            resetCodeUsage(user)
          }
          if (settings."userCode${user}" && settings."userSlot${user}") {
            getConflicts(settings."userSlot${user}")
          }
          href(name: "toUserPage${user}", page: "userPage", params: [number: user], required: false, description: userHrefDescription(user), title: userHrefTitle(user), state: userPageState(user) )
        }
      }
      section {
        href(name: "toResetAllCodeUsage", title: "Reset Code Usage", page: "resetAllCodeUsagePage", description: "Tap to reset")
      }
    } else {
      section("Users") {
        paragraph "Users are set to zero.  Please go back to the main page and change the number of users to at least 1."
      }
    }
  }
}

def userPage(params) {
  dynamicPage(name:"userPage", title:"User Settings") {
    def i = getUser(params);

    if (!state."userState${i}".enabled) {
      section {
        paragraph "WARNING:\n\nThis user has been disabled.\nReason: ${state."userState${i}".disabledReason}"
        href(name: "toreEnableUserPage", title: "Reset User", page: "reEnableUserPage", params: [number: i], description: "Tap to reset")
      }
    }
    if (settings."userCode${i}" && settings."userSlot${i}") {
      def conflict = getConflicts(settings."userSlot${i}")
      if (conflict.has_conflict) {
        section("Conflicts:") {
          theLocks.each { lock->
            if (conflict."lock${lock.id}" && conflict."lock${lock.id}".conflicts != []) {
              paragraph "${lock.displayName} slot ${fancyString(conflict."lock${lock.id}".conflicts)}"
            }
          }
        }
      }
    }
    section("Code #${i}") {
      input(name: "userName${i}", type: "text", title: "Name for User", defaultValue: settings."userName${i}")
      def title = "Code (4 to 8 digits)"
      theLocks.each { lock->
        if (lock.hasAttribute('pinLength')) {
          title = "Code (Must be ${lock.latestValue('pinLength')} digits)"
        }
      }
      input(name: "userCode${i}", type: "text", title: title, required: false, defaultValue: settings."userCode${i}", refreshAfterSelection: true)
      input(name: "userSlot${i}", type: "number", title: "Slot (1 through 30)", defaultValue: preSlectedCode(i))
    }
    section {
      input(name: "dontNotify${i}", title: "Mute entry notification?", type: "bool", required: false, defaultValue: settings."dontNotify${i}")
      input(name: "burnCode${i}", title: "Burn after use?", type: "bool", required: false, defaultValue: settings."burnCode${i}")
      input(name: "userEnabled${i}", title: "Enabled?", type: "bool", required: false, defaultValue: settings."userEnabled${i}")
      def hhPhrases = location.getHelloHome()?.getPhrases()*.label
      if (hhPhrases) {
        hhPhrases.sort()
        input name: "userHomePhrases${i}", type: "enum", title: "Hello Home Phrase", multiple: true,required: false, options: hhPhrases, defaultValue: settings."userHomePhrases${i}", refreshAfterSelection: true
        input "userNoRunPresence${i}", "capability.presenceSensor", title: "Don't run Actions if any of these are present:", multiple: true, required: false, defaultValue: settings."userNoRunPresence${i}" || false
        input "userDoRunPresence${i}", "capability.presenceSensor", title: "Run Actions only if any of these are present:", multiple: true, required: false, defaultValue: settings."userDoRunPresence${i}" || false
      }
    }
    section {
      href(name: "toSetupPage", title: "Back To Users", page: "setupPage")
      href(name: "toResetCodeUsagePage", title: "Reset Code Usage", page: "resetCodeUsagePage", params: [number: i], description: "Tap to reset")
    }
  }
}

def preSlectedCode(i) {
  if (settings."userSlot${i}" != null) {
    return settings."userSlot${i}"
  } else {
    return i
  }
}

def notificationPage() {
  dynamicPage(name: "notificationPage", title: "Notification Settings") {

    section {
      input(name: "phone", type: "text", title: "Text This Number", description: "Phone number", required: false, submitOnChange: true)
      paragraph "For multiple SMS recipients, separate phone numbers with a semicolon(;)"
      input(name: "notification", type: "bool", title: "Send A Push Notification", description: "Notification", required: false, submitOnChange: true)
      if (phone != null || notification || sendevent) {
        input(name: "notifyAccess", title: "on User Entry", type: "bool", required: false)
        input(name: "notifyLock", title: "on Lock", type: "bool", required: false)
        input(name: "notifyUnlock", title: "on Unlock", type: "bool", required: false)
        input(name: "notifyAccessStart", title: "when granting access", type: "bool", required: false)
        input(name: "notifyAccessEnd", title: "when revoking access", type: "bool", required: false)
      }
    }

    section("Only During These Times (optional)") {
      input(name: "notificationStartTime", type: "time", title: "Notify Starting At This Time", description: null, required: false)
      input(name: "notificationEndTime", type: "time", title: "Notify Ending At This Time", description: null, required: false)
    }
  }
}

def schedulingPage() {
  dynamicPage(name: "schedulingPage", title: "Rules For Access Scheduling") {
    if (!days) {
      section {
        href(name: "toCalendarPage", title: "Calendar", page: "calendarPage", description: calendarHrefDescription(), state: calendarHrefDescription() ? "complete" : "")
      }
    }
    if (!startDay && !startMonth && !startYear && !endDay && !endMonth && !endYear) {
      section {
        input(name: "days", type: "enum", title: "Allow User Access On These Days", description: "Every day", required: false, multiple: true, options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"], submitOnChange: true)
      }
      section {
        input(name: "modeStart", title: "Allow Access when in this mode", type: "mode", required: false, mutliple: false, submitOnChange: true)
      }
      section {
        if (modeStart) {
          input "andOrTime", "enum", title: "[And/Or] at a set time?", metadata:[values:["and", "or"]], required: false, submitOnChange: true
        }
        if ((modeStart == null) || andOrTime) {
          input(name: "startTime", type: "time", title: "Start Time", description: null, required: false)
          input(name: "endTime", type: "time", title: "End Time", description: null, required: false)
        }
      }
    }
  }
}

def calendarPage() {
  dynamicPage(name: "calendarPage", title: "Calendar Access") {
    section() {
      paragraph "This page is for advanced users only. You must enter each field carefully."
      paragraph "Calendar use does not support daily grant/deny OR Modes.  You cannot both have a date here, and allow access only on certain days/modes."
    }
    def hhPhrases = location.getHelloHome()?.getPhrases()*.label
    section("Start Date") {
      input name: "startDay", type: "number", title: "Day", required: false
      input name: "startMonth", type: "number", title: "Month", required: false
      input name: "startYear", type: "number", description: "Format(yyyy)", title: "Year", required: false
      input name: "startTime", type: "time", title: "Start Time", description: null, required: false
      if (hhPhrases) {
        hhPhrases.sort()
        input name: "calStartPhrase", type: "enum", title: "Hello Home Phrase", multiple: true,required: false, options: hhPhrases, refreshAfterSelection: true
      }
    }
    section("End Date") {
      input name: "endDay", type: "number", title: "Day", required: false
      input name: "endMonth", type: "number", title: "Month", required: false
      input name: "endYear", type: "number", description: "Format(yyyy)", title: "Year", required: false
      input name: "endTime", type: "time", title: "End Time", description: null, required: false
      if (hhPhrases) {
        hhPhrases.sort()
        input name: "calEndPhrase", type: "enum", title: "Hello Home Phrase", multiple: true,required: false, options: hhPhrases, refreshAfterSelection: true
      }
    }
  }
}

def onUnlockPage() {
  dynamicPage(name:"onUnlockPage", title:"Global Actions (Any Code)") {
    section("Actions") {
      def hhPhrases = location.getHelloHome()?.getPhrases()*.label
      if (hhPhrases) {
        hhPhrases.sort()
        input name: "homePhrases", type: "enum", title: "Home Mode Phrase", multiple: true, required: false, options: hhPhrases, refreshAfterSelection: true, submitOnChange: true
        if (homePhrases) {
          input "noRunPresence", "capability.presenceSensor", title: "Don't run Actions if any of these are present:", multiple: true, required: false
          input "doRunPresence", "capability.presenceSensor", title: "Run Actions only if any of these are present:", multiple: true, required: false
          input name: "manualUnlock", title: "Initiate phrase on manual unlock also?", type: "bool", defaultValue: false, refreshAfterSelection: true
          input(name: "modeIgnore", title: "Do not run Routine when in this mode", type: "mode", required: false, mutliple: false)
        }
      }
    }
  }
}

def resetCodeUsagePage(params) {
  def i = getUser(params)
  // do reset
  resetCodeUsage(i)

  dynamicPage(name:"resetCodeUsagePage", title:"User Usage Reset") {
    section {
      paragraph "User code usage has been reset."
    }
    section {
      href(name: "toSetupPage", title: "Back To Users", page: "setupPage")
    }
  }
}

def resetAllCodeUsagePage() {
  // do resetAll
  resetAllCodeUsage()
  dynamicPage(name:"resetAllCodeUsagePage", title:"User Settings") {
    section {
      paragraph "All user code usages have been reset."
    }
    section("Users") {
      href(name: "toSetupPage", title: "Back to Users", page: "setupPage")
      href(name: "toRootPage", title: "Main Page", page: "rootPage")
    }
  }
}

def reEnableUserPage(params) {
  // do reset
  def i = getUser(params)
  enableUser(i)
  lockErrorLoopReset()
  dynamicPage(name:"reEnableUserPage", title:"User re-enabled") {
    section {
      paragraph "User has been enabled."
    }
    section {
      href(name: "toSetupPage", title: "Back To Users", page: "setupPage")
    }
  }
}

def getUser(params) {
  def i = 1
  // Assign params to i.  Sometimes parameters are double nested.
  if (params.number) {
    i = params.number
  } else if (params.params){
    i = params.params.number
  } else if (state.lastUser) {
    i = state.lastUser
  }

  //Make sure i is a round number, not a float.
  if ( ! i.isNumber() ) {
    i = i.toInteger();
  } else if ( i.isNumber() ) {
    i = Math.round(i * 100) / 100
  }
  state.lastUser = i
  return i
}

def getLock(params) {
  def id = ''
  // Assign params to id.  Sometimes parameters are double nested.
  if (params.id) {
    id = params.id
  } else if (params.params){
    id = params.params.id
  } else if (state.lastLock) {
    id = state.lastLock
  }

  state.lastLock = id
  return theLocks.find{it.id == id}
}

def infoPage() {
  dynamicPage(name:"infoPage", title:"Lock Info") {
    section() {
      href(name: "toInfoRefreshPage", page: "infoRefreshPage", title: "Refresh Lock Data", description: 'Tap to refresh')
    }
    section("Locks") {
      if (theLocks) {
        def i = 0
        theLocks.each { lock->
          i++
          href(name: "toLockInfoPage${i}", page: "lockInfoPage", params: [id: lock.id], required: false, title: lock.displayName )
        }
      }
    }
  }
}

def infoRefreshPage() {
  dynamicPage(name:"infoRefreshPage", title:"Lock Info") {
    section() {
      manualPoll()
      paragraph "Lock info refreshing soon."
      href(name: "toInfoPage", page: "infoPage", title: "Back to Lock Info")
    }
  }
}

def lockInfoPage(params) {
  dynamicPage(name:"lockInfoPage", title:"Lock Info") {

    def lock = getLock(params)
    if (lock) {
      section("${lock.displayName}") {
        if (state."lock${lock.id}".codes != null) {
          def i = 0
          def pass = ''
          state."lock${lock.id}".codes.each { code->
            i++
            pass = state."lock${lock.id}".codes."slot${i}"
            paragraph "Slot ${i}\nCode: ${pass}"
          }
        } else {
          paragraph "No Lock data received yet.  Requires custom device driver.  Will be populated on next poll event."
        }
      }
    }
  }
}


def keypadPage() {
  dynamicPage(name: "keypadPage",title: "Keypad Settings (optional)") {
    section("Settings") {
      // TODO: put inputs here
      input(name: "keypad", title: "Keypad", type: "capability.lockCodes", multiple: true, required: false)
    }
    def hhPhrases = location.getHelloHome()?.getPhrases()*.label
    hhPhrases?.sort()
    section("Routines", hideable: true, hidden: true) {
      input(name: "armRoutine", title: "Arm/Away routine", type: "enum", options: hhPhrases, required: false)
      input(name: "disarmRoutine", title: "Disarm routine", type: "enum", options: hhPhrases, required: false)
      input(name: "stayRoutine", title: "Arm/Stay routine", type: "enum", options: hhPhrases, required: false)
      input(name: "nightRoutine", title: "Arm/Night routine", type: "enum", options: hhPhrases, required: false)
      input(name: "armDelay", title: "Arm Delay (in seconds)", type: "number", required: false)
      input(name: "notifyIncorrectPin", title: "Notify you when incorrect code is used?", type: "bool", required: false)
    }
  }
}

public smartThingsDateFormat() { "yyyy-MM-dd'T'HH:mm:ss.SSSZ" }

public humanReadableStartDate() {
  new Date().parse(smartThingsDateFormat(), startTime).format("h:mm a", timeZone(startTime))
}
public humanReadableEndDate() {
  new Date().parse(smartThingsDateFormat(), endTime).format("h:mm a", timeZone(endTime))
}

def manualPoll() {
  theLocks.poll()
}

def getConflicts(i) {
  def currentCode = settings."userCode${i}"
  def currentSlot = settings."userSlot${i}"
  def conflict = [:]
  conflict.has_conflict = false


  theLocks.each { lock->
    if (state."lock${lock.id}".codes) {
      conflict."lock${lock.id}" = [:]
      conflict."lock${lock.id}".conflicts = []
      def ind = 0
      state."lock${lock.id}".codes.each { code ->
        ind++
        if (currentSlot?.toInteger() != ind.toInteger() && !isUnique(currentCode, state."lock${lock.id}".codes."slot${ind}")) {
          conflict.has_conflict = true
          state."userState${i}".enabled = false
          state."userState${i}".disabledReason = "Code Conflict Detected"
          conflict."lock${lock.id}".conflicts << ind
        }
      }
    }
  }

  return conflict
}

def isUnique(newInt, oldInt) {

  if (newInt == null || oldInt == null) {
    // if either number is null, break here.
    return true
  }

  if (!newInt.isInteger() || !oldInt.isInteger()) {
    // number is not an integer, can't check.
    return true
  }

  def newArray = []
  def oldArray = []
  def result = true

  def i = 0
  // Get a normalized sequence, at the same length
  newInt.toString().toList().collect {
    i++
    if (i <= oldInt.length()) {
      newArray << normalizeNumber(it.toInteger())
    }
  }

  i = 0
  oldInt.toString().toList().collect {
    i++
    if (i <= oldInt.length()) {
      oldArray << normalizeNumber(it.toInteger())
    }
  }

  i = 0
  newArray.each { num->
    i++
    if (newArray.join() == oldArray.join()) {
      // The normalized numbers are the same!
      result = false
    }
  }
  return result
}

def normalizeNumber(number) {
  def result = null
  // RULE: Since some locks share buttons, make sure unique.
  // Even locks with 10-keys follow this rule! (annoyingly)
  switch (number) {
    case [1,2]:
      result = 1
      break
    case [3,4]:
      result = 2
      break
    case [5,6]:
      result = 3
      break
    case [7,8]:
      result = 4
      break
    case [9,0]:
      result = 5
      break
  }
  return result
}

def setupPageDescription(){
  def parts = []
  for (int i = 1; i <= settings.maxUsers; i++) {
    parts << settings."userName${i}"
  }
  return fancyString(parts)
}

def notificationPageDescription() {
  def parts = []
  def msg = ""
  if (settings.phone) {
    parts << "SMS to ${phone}"
  }
  if (settings.sendevent) {
    parts << "Event Notification"
  }
  if (settings.notification) {
    parts << "Push Notification"
  }
  msg += fancyString(parts)
  parts = []

  if (settings.notifyAccess) {
    parts << "on entry"
  }
  if (settings.notifyLock) {
    parts << "on lock"
  }
  if (settings.notifyUnlock) {
    parts << "on unlock"
  }
  if (settings.notifyAccessStart) {
    parts << "when granting access"
  }
  if (settings.notifyAccessEnd) {
    parts << "when revoking access"
  }
  if (settings.notificationStartTime) {
    parts << "starting at ${settings.notificationStartTime}"
  }
  if (settings.notificationEndTime) {
    parts << "ending at ${settings.notificationEndTime}"
  }
  if (parts.size()) {
    msg += ": "
    msg += fancyString(parts)
  }
  return msg
}

def calendarHrefDescription() {
  def dateStart = startDateTime()
  def dateEnd = endDateTime()
  if (dateEnd && dateStart) {
    def startReadableTime = readableDateTime(dateStart)
    def endReadableTime = readableDateTime(dateEnd)
    return "Accessible from ${startReadableTime} until ${endReadableTime}"
  } else if (!dateEnd && dateStart) {
    def startReadableTime = readableDateTime(dateStart)
    return "Accessible on ${startReadableTime}"
  } else if (dateEnd && !dateStart){
    def endReadableTime = readableDateTime(dateEnd)
    return "Accessible until ${endReadableTime}"
  }
}

def readableDateTime(date) {
  new Date().parse(smartThingsDateFormat(), date.format(smartThingsDateFormat(), location.timeZone)).format("EEE, MMM d yyyy 'at' h:mma", location.timeZone)
}

def userHrefTitle(i) {
  def title = "User ${i}"
  if (settings."userName${i}") {
    title = settings."userName${i}"
  }
  return title
}

def userHrefDescription(i) {
  def uc = settings."userCode${i}"
  def us = settings."userSlot${i}"
  def usage = state."userState${i}".usage
  def description = ""
  if (us != null) {
    description += "Slot: ${us}"
  }
  if (uc != null) {
    description += " / ${uc}"
    if(settings."burnCode${i}") {
      description += ' [Single Use]'
    }
  }
  if (usage != null) {
    description += " [Usage: ${usage}]"
  }
  return description
}

def userPageState(i) {
  if (settings."userCode${i}" && userIsEnabled(i)) {
    if (settings."burnCode${i}") {
      if (state."userState${i}".usage > 0) {
        return 'incomplete'
      } else {
        return 'complete'
      }
    } else {
      return 'complete'
    }

  } else if (settings."userCode${i}" && !settings."userEnabled${i}") {
    return 'incomplete'
  } else {
    return 'incomplete'
  }
}

def userIsEnabled(i) {
  if (settings."userEnabled${i}" && (settings."userCode${i}" != null) && (state."userState${i}".enabled != false)) {
    return true
  } else {
    return false
  }
}

def fancyDeviceString(devices = []) {
  fancyString(devices.collect { deviceLabel(it) })
}

def deviceLabel(device) {
  return device.label ?: device.name
}

def fancyString(listOfStrings) {
  listOfStrings.removeAll([null])
  def fancify = { list ->
    return list.collect {
      def label = it
      if (list.size() > 1 && it == list[-1]) {
        label = "and ${label}"
      }
      label
    }.join(", ")
  }

  return fancify(listOfStrings)
}

def schedulingHrefDescription() {
  if (startDateTime() || endDateTime()) {
    calendarHrefDescription()
  } else {
    def descriptionParts = []
    if (days) {
      descriptionParts << "On ${fancyString(days)},"
    }

    descriptionParts << "${fancyDeviceString(theLocks)} will be accessible"
    if ((andOrTime != null) || (modeStart == null)) {
      if (startTime) {
        descriptionParts << "at ${humanReadableStartDate()}"
      }
      if (endTime) {
        descriptionParts << "until ${humanReadableEndDate()}"
      }
    }

    if (modeStart) {
      if (startTime && andOrTime) {
        descriptionParts << andOrTime
      }
      descriptionParts << "when ${location.name} enters '${modeStart}' mode"
    }

    if (descriptionParts.size() <= 1) {
      // locks will be in the list no matter what. No rules are set if only locks are in the list
      return null
    }
    return descriptionParts.join(" ")
  }
}

def installed() {
  log.debug "Installing 'Locks' with settings: ${settings}"
  initialize()
}

def updated() {
  log.debug "Updating 'Locks' with settings: ${settings}"
  initialize()
}

private initialize() {
  unsubscribe()
  unschedule()
  if (startTime && !startDateTime()) {
    log.debug "scheduling access routine to run at ${startTime}"
    schedule(startTime, "reconcileCodesStart")
  } else if (startDateTime()) {
    // There's a start date, so let's run then
    log.debug "scheduling RUNONCE start"
    runOnce(startDateTime().format(smartThingsDateFormat(), location.timeZone), "reconcileCodesStart")
  }

  if (endTime && !endDateTime()) {
    log.debug "scheduling access denial routine to run at ${endTime}"
    schedule(endTime, "reconcileCodesEnd")
  } else if (endDateTime()) {
    // There's a end date, so let's run then
    log.debug "scheduling RUNONCE end"
    runOnce(endDateTime().format(smartThingsDateFormat(), location.timeZone), "reconcileCodesEnd")
  }

  subscribe(location, locationHandler)

  subscribe(theLocks, "codeReport", codereturn)
  subscribe(theLocks, "lock", codeUsed)
  subscribe(theLocks, "reportAllCodes", pollCodeReport, [filterEvents:false])
  if (keypad) {
    subscribe(location,"alarmSystemStatus",alarmStatusHandler)
    subscribe(keypad,"codeEntered",codeEntryHandler)
  }

  revokeDisabledUsers()
  reconcileCodes()
  lockErrorLoopReset()
  initalizeLockData()

  log.debug "state: ${state}"
}

def resetAllCodeUsage() {
  for (int i = 1; i <= settings.maxUsers; i++) {
    lockErrorLoopReset()
    resetCodeUsage(i)
  }
  log.debug "reseting all code usage"
}

def resetCodeUsage(i) {
  if(state."userState${i}" == null) {
    state."userState${i}" = [:]
    state."userState${i}".enabled = true
  }
  state."userState${i}".usage = 0
}

def enableUser(i) {
  state."userState${i}".enabled = true
}

def initalizeLockData() {
  theLocks.each { lock->
    if (state."lock${lock.id}" == null) {
      state."lock${lock.id}" = [:]
    }
  }
}

def lockErrorLoopReset() {
  state.error_loop_count = 0
  theLocks.each { lock->
    if (state."lock${lock.id}" == null) {
      state."lock${lock.id}" = [:]
    }
    state."lock${lock.id}".error_loop = false
  }
}


def locationHandler(evt) {
  log.debug "locationHandler evt: ${evt.value}"
  if (modeStart) {
    reconcileCodes()
  }
}

def reconcileCodes() {
  if (isAbleToStart()) {
    grantAccess()
  } else {
    revokeAccess()
  }
}

def reconcileCodesStart() {
  // schedule start of reconcileCodes
  reconcileCodes()
  if (calStartPhrase) {
    location.helloHome.execute(calStartPhrase)
  }
}

def reconcileCodesEnd() {
  // schedule end of reconcileCodes
  reconcileCodes()
  if (calEndPhrase) {
    location.helloHome.execute(calEndPhrase)
  }
}

def isAbleToStart() {
  def dateStart = startDateTime()
  def dateEnd = endDateTime()

  if (dateStart || dateEnd) {
    // calendar schedule above all
    return checkCalendarSchedule(dateStart, dateEnd)
  } else if (modeStart || startTime || endTime || days) {
    // No calendar set, check daily schedule
    if (isCorrectDay()) {
      // it's the right day
      checkDailySchedule()
    } else {
      // it's the wrong day
      return false
    }
  } else {
    // no schedule
    return true
  }
}

def checkDailySchedule() {
  if (andOrTime && modeStart && (isCorrectMode() || isInScheduledTime())) {
    // in correct mode or time with and/or switch
    if (andOrTime == 'and') {
      // must be both
      if (isCorrectMode() && isInScheduledTime()) {
        // is both
        return true
      } else {
        // is not both
        return false
      }
    } else {
      // could be either
      if (isCorrectMode() || isInScheduledTime()) {
        // it is either mode or time
        return true
      } else {
        // is not either mode or time
        return false
      }
    }
  } else {
    // Allow either mode or time, no andOrTime is set
    if (isCorrectMode() || isInScheduledTime()) {
      // it is either mode or time
      return true
    } else {
      // is not either mode or time
      return false
    }
  }
}

def checkCalendarSchedule(dateStart, dateEnd) {
  def now = rightNow().getTime()
  if (dateStart && !dateEnd) {
    // There's a start time, but no end time.  Allow access after start
    if (dateStart.getTime() > now) {
      // It's after the start time
      return true
    } else {
      // It's before the start time
      return false
    }

  } else if (dateEnd && !dateStart) {
    // There's a end time, but no start time.  Allow access until end
    if (dateStart.getTime() > now) {
      // It's after the start time
      return true
    } else {
      // It's before the start time
      return false
    }

  } else {
    // There's both an end time, and a start time.  Allow access between them.
    if (dateStart.getTime() < now && dateEnd.getTime() > now) {
      // It's in calendar times
      return true
    } else {
      // It's not in calendar times
      return false
    }
  }
}

def isCorrectMode() {
  if (modeStart) {
    // mode check is on
    if (location.mode == modeStart) {
      // we're in the right one mode
      return true
    } else {
      // we're in the wrong mode
      return false
    }
  } else {
    // mode check is off
    return false
  }
}

def isInScheduledTime() {
  def now = new Date()
  if (startTime && endTime) {
    def start = timeToday(startTime)
    def stop = timeToday(endTime)

    // there's both start time and end time
    if (start.before(now) && stop.after(now)){
      // It's between the times
      return true
    } else {
      // It's not between the times
      return false
    }
  } else if (startTime && !endTime){
    // there's a start time, but no end time
    def start = timeToday(startTime)
    if (start.before(now)) {
      // it's after start time
      return true
    } else {
      //it's before start time
      return false
    }
  } else if (!startTime && endTime) {
    // there's an end time but no start time
    def stop = timeToday(endTime)
    if (stop.after(now)) {
      // it's still before end time
      return true
    } else {
      // it's after end time
      return false
    }
  } else {
    // there are no times
    return false
  }
}

def startDateTime() {
  if (startDay && startMonth && startYear && startTime) {
    def time = new Date().parse(smartThingsDateFormat(), startTime).format("'T'HH:mm:ss.SSSZ", timeZone(startTime))
    return Date.parse("yyyy-MM-dd'T'HH:mm:ss.SSSZ", "${startYear}-${startMonth}-${startDay}${time}")
  } else {
    // Start Date Time not set
    return false
  }
}

def endDateTime() {
  if (endDay && endMonth && endYear && endTime) {
    def time = new Date().parse(smartThingsDateFormat(), endTime).format("'T'HH:mm:ss.SSSZ", timeZone(endTime))
    return Date.parse("yyyy-MM-dd'T'HH:mm:ss.SSSZ", "${endYear}-${endMonth}-${endDay}${time}")
  } else {
    // End Date Time not set
    return false
  }
}

def rightNow() {
  def now = new Date().format("yyyy-MM-dd'T'HH:mm:ss.SSSZ", location.timeZone)
  return Date.parse("yyyy-MM-dd'T'HH:mm:ss.SSSZ", now)
}

def isCorrectDay() {
  def today = new Date().format("EEEE", location.timeZone)
  log.debug "today: ${today}, days: ${days}"
  if (!days || days.contains(today)) {
    // if no days, assume every day
    return true
  }
  log.trace "should not allow access - Not correct Day"
  return false
}

def userSlotArray() {
  def array = []
  for (int i = 1; i <= settings.maxUsers; i++) {
    if (settings."userSlot${i}") {
      array << settings."userSlot${i}".toInteger()
    }
  }
  return array
}

def enabledUsersArray() {
  def array = []
  for (int i = 1; i <= settings.maxUsers; i++) {
    if (userIsEnabled(i)) {
      array << i
    }
  }
  return array
}
def enabledUsersSlotArray() {
  def array = []
  for (int i = 1; i <= settings.maxUsers; i++) {
    if (userIsEnabled(i)) {
      def userSlot = settings."userSlot${i}"
      array << userSlot.toInteger()
    }
  }
  return array
}

def disabledUsersSlotArray() {
  def array = []
  for (int i = 1; i <= settings.maxUsers; i++) {
    if (!userIsEnabled(i)) {
      if (settings."userSlot${i}") {
        array << settings."userSlot${i}".toInteger()
      }
    }
  }
  return array
}

def codereturn(evt) {
  def codeNumber = evt.data.replaceAll("\\D+","")
  def codeSlot = evt.value
  if (notifyAccessEnd || notifyAccessStart) {
    if (userSlotArray().contains(evt.integerValue.toInteger())) {
      def userName = settings."userName${usedUserIndex(evt.integerValue)}"
      if (codeNumber == "") {
        if (notifyAccessEnd) {
          def message = "${userName} no longer has access to ${evt.displayName}"
          if (codeNumber.isNumber()) {
            state."lock${evt.deviceId}".codes."slot${codeSlot}" = codeNumber
          }
          send(message)
        }
      } else {
        if (notifyAccessStart) {
          def message = "${userName} now has access to ${evt.displayName}"
          if (codeNumber.isNumber()) {
            state."lock${evt.deviceId}".codes."slot${codeSlot}" = codeNumber
          }
          send(message)
        }
      }
    }
  }
}

def usedUserIndex(usedSlot) {
  for (int i = 1; i <= settings.maxUsers; i++) {
    if (settings."userSlot${i}" && settings."userSlot${i}".toInteger() == usedSlot.toInteger()) {
      return i
    }
  }
  return false
}

def codeUsed(evt) {
  // check the status of the lock, helpful for some schlage locks.
  runIn(10, doPoll)
  log.debug("codeUsed evt.value: " + evt.value + ". evt.data: " + evt.data)
  def message = null

  if(evt.value == "unlocked" && evt.data) {
    def codeData = new JsonSlurper().parseText(evt.data)
    if(codeData.usedCode && codeData.usedCode.isNumber() && userSlotArray().contains(codeData.usedCode.toInteger())) {
      def usedIndex = usedUserIndex(codeData.usedCode).toInteger()
      def unlockUserName = settings."userName${usedIndex}"
      message = "${evt.displayName} was unlocked by ${unlockUserName}"
      // increment usage
      state."userState${usedIndex}".usage = state."userState${usedIndex}".usage + 1
      if(settings."userHomePhrases${usedIndex}") {
        // Specific User Hello Home
        if (settings."userNoRunPresence${usedIndex}" && settings."userDoRunPresence${usedIndex}" == null) {
          if (!anyoneHome(settings."userNoRunPresence${usedIndex}")) {
            location.helloHome.execute(settings."userHomePhrases${usedIndex}")
          }
        } else if (settings."userDoRunPresence${usedIndex}" && settings."userNoRunPresence${usedIndex}" == null) {
          if (anyoneHome(settings."userDoRunPresence${usedIndex}")) {
            location.helloHome.execute(settings."userHomePhrases${usedIndex}")
          }
        } else if (settings."userDoRunPresence${usedIndex}" && settings."userNoRunPresence${usedIndex}") {
          if (anyoneHome(settings."userDoRunPresence${usedIndex}") && !anyoneHome(settings."userNoRunPresence${usedIndex}")) {
            location.helloHome.execute(settings."userHomePhrases${usedIndex}")
          }
        } else {
          location.helloHome.execute(settings."userHomePhrases${usedIndex}")
        }
      }
      if(settings."burnCode${usedIndex}") {
        theLocks.deleteCode(codeData.usedCode)
        runIn(60*2, doPoll)
        message += ".  Now burning code."
      }
      //Don't send notification if muted
      if(settings."dontNotify${usedIndex}" == true) {
        message = null
      }
    }
  } else if(evt.value == "unlocked" && settings.notifyUnlock) {
    message = "${evt.displayName} has been manually unlocked"
  } else if(evt.value == "locked" && settings.notifyLock) {
    message = "${evt.displayName} has been locked"
  }

  if (message) {
    log.debug("Sending message: " + message)
    send(message)
  }

  if (homePhrases) {
    performActions(evt)
  }
}

def performActions(evt) {
  if(evt.value == "unlocked" && evt.data) {
    def codeData = new JsonSlurper().parseText(evt.data)
    if(enabledUsersArray().contains(codeData.usedCode) || isManualUnlock(codeData)) {
      // Global Hello Home
      if(location.currentMode != modeIgnore) {
        if (noRunPresence && doRunPresence == null) {
          if (!anyoneHome(noRunPresence)) {
            location.helloHome.execute(homePhrases)
          }
        } else if (doRunPresence && noRunPresence == null) {
          if (anyoneHome(doRunPresence)) {
            location.helloHome.execute(homePhrases)
          }
        } else if (doRunPresence && noRunPresence) {
          if (anyoneHome(doRunPresence) && !anyoneHome(noRunPresence)) {
            location.helloHome.execute(homePhrases)
          }
        } else {
         location.helloHome.execute(homePhrases)
        }
      } else {
        def routineMessage = "Already in ${modeIgnore} mode, skipping execution of ${homePhrases} routine."
        log.debug routineMessage
        send(routineMessage)
      }
    }
  }
}

def revokeDisabledUsers() {
  def array = []
  disabledUsersSlotArray().each { slot ->
    array << ["code${slot}", ""]
  }
  def json = new groovy.json.JsonBuilder(array).toString()
  if (json != '[]') {
    theLocks.updateCodes(json)
    runIn(60*2, doPoll)
  }
}

def doPoll() {
  // this gets codes if custom device is installed
  if (!allCodesDone()) {
    state.error_loop_count = state.error_loop_count + 1
  }
  theLocks.poll()
}

def grantAccess() {
  def array = []
  enabledUsersArray().each { user->
    def userSlot = settings."userSlot${user}"
    if (settings."userCode${user}" != null) {
      def newCode = settings."userCode${user}"
      array << ["code${userSlot}", "${newCode}"]
    } else {
      array << ["code${userSlot}", ""]
    }
  }
  def json = new groovy.json.JsonBuilder(array).toString()
  if (json != '[]') {
    theLocks.updateCodes(json)
    runIn(60*2, doPoll)
  }
}

def revokeAccess() {
  def array = []
  enabledUsersArray().each { user->
    def userSlot = settings."userSlot${user}"
    array << ["code${userSlot}", ""]
  }
  def json = new groovy.json.JsonBuilder(array).toString()
  if (json != '[]') {
    theLocks.updateCodes(json)
    runIn(60*2, doPoll)
  }
}

def isManualUnlock(codeData) {
  // check to see if the user wants this
  if (manualUnlock) {
    // garyd9's device type returns 'manual'
    if ((codeData.usedCode == "") || (codeData.usedCode == null) || (codeData.usedCode == 'manual')) {
      // no code used on unlock!
      return true
    } else {
      // probably a code we're not dealing with here
      return false
    }
  } else {
    return false
  }
}

def isActiveBurnCode(slot) {
  if (settings."burnCode${slot}" && state."userState${slot}".usage > 0) {
    return false
  } else {
    // not a burn code / not yet used
    return true
  }
}

def pollCodeReport(evt) {
  def active = isAbleToStart()
  def codeData = new JsonSlurper().parseText(evt.data)
  def numberOfCodes = codeData.codes
  def userSlots = userSlotArray()

  def array = []

  (1..maxUsers).each { user->
    def slot = settings."userSlot${user}"
    def code = codeData."code${slot}"
    def correctCode = settings."userCode${user}"
    if (active) {
      if (userIsEnabled(user) && isActiveBurnCode(user)) {
        if (code == settings."userCode${user}") {
          // Code is Active, We should be active. Nothing to do
        } else {
          // Code is incorrect, We should be active.
          array << ["code${slot}", settings."userCode${user}"]
        }
      } else {
        if (code != '') {
          // Code is set, user is disabled, We should be disabled.
          array << ["code${slot}", ""]
        } else {
          // Code is not set, user is disabled. Nothing to do
        }
      }
    } else {
      if (code != '') {
        // Code is set, We should be disabled.
        array << ["code${slot}", ""]
      } else {
        // Code is not active, We should be disabled. Nothing to do
      }
    }
  }

  def currentLock = theLocks.find{it.id == evt.deviceId}
  populateDiscovery(codeData, currentLock)

  def json = new groovy.json.JsonBuilder(array).toString()
  if (json != '[]') {
    runIn(60*2, doPoll)

    //Lock is in an error state
    state."lock${currentLock.id}".error_loop = true
    def error_number = state.error_loop_count + 1
    if (error_number <= 10) {
      log.debug "sendCodes fix is: ${json} Error: ${error_number}/10"
      currentLock.updateCodes(json)
    } else {
      log.debug "kill fix is: ${json}"
      currentLock.updateCodes(json)
      json = new JsonSlurper().parseText(json)
      def n = 0
      json.each { code ->
        n = code[0][4..-1].toInteger()
        def usedIndex = usedUserIndex(n)
        def name = settings."userName${usedIndex}"
        if (state."userState${usedIndex}".enabled) {
          state."userState${usedIndex}".enabled = false
          state."userState${usedIndex}".disabledReason = "Controller failed to set code"
          send("Controller failed to set code for ${name}")
        }
      }
    }
  } else {
    state."lock${currentLock.id}".error_loop = false
    if (allCodesDone) {
      lockErrorLoopReset()
    } else {
      runIn(60, doPoll)
    }
  }
}

def allCodesDone() {
  def i = 0
  def codeComplete = true
  theLocks.each { lock->
    i++
    if (state."lock${lock.id}".error_loop == true) {
      codeComplete = false
    }
  }
  return codeComplete
}

private anyoneHome(sensors) {
  def result = false
  if(sensors.findAll { it?.currentPresence == "present" }) {
    result = true
  }
  result
}

private send(msg) {
  if (notificationStartTime != null && notificationEndTime != null) {
    def start = timeToday(notificationStartTime)
    def stop = timeToday(notificationEndTime)
    def now = new Date()
    if (start.before(now) && stop.after(now)){
      sendMessage(msg)
    }
  } else {
    sendMessage(msg)
  }
}

private sendMessage(msg) {
  if (notification) {
    sendPush(msg)
  } else {
    sendNotificationEvent(msg)
  }
  if (phone) {
    if ( phone.indexOf(";") > 1){
      def phones = phone.split(";")
      for ( def i = 0; i < phones.size(); i++) {
        sendSms(phones[i], msg)
      }
    }
    else {
      sendSms(phone, msg)
    }
  }
}

def populateDiscovery(codeData, lock) {
  def codes = [:]
  def codeSlots = 30
  if (codeData.codes) {
    codeSlots = codeData.codes
  }
  (1..codeSlots).each { slot->
    codes."slot${slot}" = codeData."code${slot}"
  }
  state."lock${lock.id}".codes = codes
}

private String getPIN() {
  return settings.pin.value.toString().padLeft(4,'0')
}

def alarmStatusHandler(event) {
  log.debug "Keypad manager caught alarm status change: "+event.value
  if (event.value == "off"){
    keypad?.setDisarmed()
  }
  else if (event.value == "away"){
    keypad?.setArmedAway()
  }
  else if (event.value == "stay") {
    keypad?.setArmedStay()
  }
}

private sendSHMEvent(String shmState) {
  def event = [
        name:"alarmSystemStatus",
        value: shmState,
        displayed: true,
        description: "System Status is ${shmState}"
      ]
  log.debug "test ${event}"
  sendLocationEvent(event)
}

private execRoutine(armMode) {
  if (armMode == 'away') {
    location.helloHome?.execute(settings.armRoutine)
  } else if (armMode == 'stay') {
    location.helloHome?.execute(settings.stayRoutine)
  } else if (armMode == 'off') {
    location.helloHome?.execute(settings.disarmRoutine)
  }
}

def codeEntryHandler(evt) {
  //do stuff
  log.debug "Caught code entry event! ${evt.value.value}"

  def codeEntered = evt.value as String

  def data = evt.data as String
  def armMode = ''
  def currentarmMode = keypad.currentValue("armMode")
  def changedMode = 0

  if (data == '0') {
    armMode = 'off'
  }
  else if (data == '3') {
    armMode = 'away'
  }
  else if (data == '1') {
    armMode = 'stay'
  }
  else if (data == '2') {
    armMode = 'stay' //Currently no separate night mode for SHM, set to 'stay'
  } else {
    log.error "${app.label}: Unexpected arm mode sent by keypad!: "+data
    return []
  }

  def i = settings.maxUsers
  def message = " "
  while (i > 0) {
    log.debug "i =" + i
    def correctCode = settings."userCode${i}" as String

    if (codeEntered == correctCode) {

      log.debug "User Enabled: " + state."userState${i}".enabled

      if (state."userState${i}".enabled == true) {
        log.debug "Correct PIN entered. Change SHM state to ${armMode}"
        //log.debug "Delay: ${armDelay}"
        //log.debug "Data: ${data}"
        //log.debug "armMode: ${armMode}"

        def unlockUserName = settings."userName${i}"

        if (data == "0") {
          //log.debug "sendDisarmCommand"
          runIn(0, "sendDisarmCommand")
          message = "${evt.displayName} was disarmed by ${unlockUserName}"
        }
        else if (data == "1") {
          //log.debug "sendStayCommand"
          runIn(armDelay, "sendStayCommand")
          message = "${evt.displayName} was armed to 'Stay' by ${unlockUserName}"
        }
        else if (data == "2") {
          //log.debug "sendNightCommand"
          runIn(armDelay, "sendNightCommand")
          message = "${evt.displayName} was armed to 'Night' by ${unlockUserName}"
        }
        else if (data == "3") {
          //log.debug "sendArmCommand"
          runIn(armDelay, "sendArmCommand")
          message = "${evt.displayName} was armed to 'Away' by ${unlockUserName}"
        }

        if(settings."burnCode${i}") {
          state."userState${i}".enabled = false
          message += ".  Now burning code."
        }

        log.debug "${message}"
        //log.debug "Initial Usage Count:" + state."userState${i}".usage
        state."userState${i}".usage = state."userState${i}".usage + 1
        //log.debug "Final Usage Count:" + state."userState${i}".usage
        send(message)
        i = 0
      } else if (state."userState${i}".enabled == false){
        log.debug "PIN Disabled"
        //Could also call acknowledgeArmRequest() with a parameter of 4 to report invalid code. Opportunity to simplify code?
        //keypad.sendInvalidKeycodeResponse()
      }
    }
    changedMode = 1
    i--
  }
  if (changedMode == 1 && i == 0) {
    def errorMsg = "Incorrect Code Entered: ${codeEntered}"
    if (notifyIncorrectPin) {
      log.debug "Incorrect PIN"
      send(errorMsg)
    }
    //Could also call acknowledgeArmRequest() with a parameter of 4 to report invalid code. Opportunity to simplify code?
    keypad.sendInvalidKeycodeResponse()
  }
}
def sendArmCommand() {
  log.debug "Sending Arm Command."
  keypad.acknowledgeArmRequest(3)
  sendSHMEvent("away")
  execRoutine("away")
}
def sendDisarmCommand() {
  log.debug "Sending Disarm Command."
  keypad.acknowledgeArmRequest(0)
  sendSHMEvent("off")
  execRoutine("off")
}
def sendStayCommand() {
  log.debug "Sending Stay Command."
  keypad.acknowledgeArmRequest(1)
  sendSHMEvent("stay")
  execRoutine("stay")
}
def sendNightCommand() {
  log.debug "Sending Night Command."
  keypad.acknowledgeArmRequest(2)
  sendSHMEvent("stay")
  execRoutine("stay")
}