/*
 *  Copyright © 2016, 2017, Oracle and/or its affiliates. All rights reserved.
 *  This software is dual-licensed to you under the MIT License (MIT) and
 *  the Universal Permissive License (UPL). See the LICENSE file in the root
 *  directory for license terms. You may choose either license, or both.
 */
import Foundation
import DeviceLib

public class Camera {
    var hardwareId:String!
    var description:String = "A motion activated camera"
    init(id:String) {
        self.hardwareId = id
    }
}

/**
 * All files uploaded to the storage clound will be placed in a subdirectory 
 * named with the device endpoint id. The library configuration property
 * Config.DISABLE_STORAGE_OBJECT_PREFIX controls this behavior. By default it
 * is set to false.
 * If Config.DISABLE_STORAGE_OBJECT_PREFIX is set to true, all uploaded files
 * will be placed in the top level directory of the storage cloud.
 * To change Config.DISABLE_STORAGE_OBJECT_PREFIX the value must be changed
 * and the DeviceLib recompiled.
 *
 * Set the propery UNIQUE_FILENAME to true prevent overwriting files in the 
 * top level directory of the storage cloud when setting 
 * Config.DISABLE_STORAGE_OBJECT_PREFIX to true. This will add the device
 * endpoint id to the filename.
 */
let UNIQUE_FILENAME:Bool = false

let MOTION_ACTIVATED_CAMERA_MODEL_URN =
    "urn:com:oracle:iot:device:motion_activated_camera"
let IMAGE_ATTRIBUTE = "image"
let IMAGE_TIME_ATTRIBUTE = "imageTime"

let ORACLE_IOT_PNG = "oracle_iot.png"

@objc
public class MotionActivatedCamera : NSObject {
    
    private class MACVSyncCallback : SyncCallback {
        var block:((SyncEvent)->())!
        override init() {
            super.init()
        }
        convenience init(block: @escaping (SyncEvent)->()) {
            self.init()
            self.block = block
        }
        override public func onSync(event:SyncEvent) {
            block(event)
        }
    }
    private var syncCallback:MACVSyncCallback?

    
    var dcd: DirectlyConnectedDevice?
    private var virtualMAC: VirtualDevice?
    private var updateGUI:(String?,String?)->() = {_,_  in }

    private var camera:Camera? = nil
    @objc let captureInterval = 20.0
    @objc var imageIndex = 0
    @objc var capturePhotoTimer:Timer? = nil
    
    override
    public init() {
        super.init()
        syncCallback = MACVSyncCallback(block: { (event) in
            print("### In onSync callback ####")
            
            let virtualDevice:VirtualDevice = event.getVirtualDevice() as! VirtualDevice
            let storageObject:StorageObject = event.getSource()
            let format = "%@ : %@ : onSync : %@ = %@"
            var msg = String(format: format,
                             ISO8601DateFormatter().string(from: Date()),
                             virtualDevice.getEndpointId(),
                             storageObject.getName(),
                             "\(storageObject.getSyncStatus())")
            
            if storageObject.getSyncStatus() == SyncStatus.inSync {
                msg += " (\(storageObject.getLength())) bytes"
            }
            self.updateGUI(nil, msg)
        })
    }

    @objc convenience
    public init(callback: @escaping (String?,String?)->()) {
        self.init()
        updateGUI = callback
    }
    
    deinit {
        if self.dcd != nil {
            do {
                try dcd!.close()
            } catch {
                print("Issue while closing Directly Connected Device.")
            }
            dcd = nil
        }
    }
    
    @objc public func start() throws -> String  {
        do {
            return try startDevice()
        } catch {
            print(error)
            throw error
        }
    }
    
    @objc public func stop() {
        capturePhotoTimer?.invalidate()
        stopDevice()
        imageIndex = 0
    }

    private func runCamera(error:ClientError?) {
        if let error = error {
            print(error)
            return
        }
        
        do {
            try virtualMAC?.setOnCall(actionName: "record",
                                      callback: self.record)
            // We know we've successfully returnef from activation or
            // creating the virtual device. Update the device id.
            self.updateGUI(self.dcd?.getEndpointId(), nil)
            virtualMAC?.setOnError(callback: onError)
            startPhotoCapture()
        } catch {
            print(error)
            return
        }
    }

    private func startDevice() throws -> String {

        dcd = try DirectlyConnectedDevice(path: Provisioning.getProvisioningFilePath(),
                                          password: Provisioning.getProvisioningPassword())
        
        // TODO: What is this for ?
        let persistentData = UserDefaults.standard
        if let hardwareId = persistentData.string(forKey: self.dcd!.getEndpointId()) {
            camera = Camera(id: hardwareId)
        } else {
            camera = Camera(id: UUID().uuidString)
            persistentData.set(camera?.hardwareId,
                               forKey: self.dcd!.getEndpointId())
        }
        do {
            if !dcd!.isActivated() {
                try self.dcd!.activate(deviceModels: MOTION_ACTIVATED_CAMERA_MODEL_URN,
                                       callback: { endpointId, error in
                    if let error = error {
                        print(error)
                        self.runCamera(error: error)
                    } else {
                        self.createVirtualDevice(endpointId: endpointId!,
                                            callback: self.runCamera)
                    }
                })

            } else {
                createVirtualDevice(endpointId: dcd!.getEndpointId(),
                                    callback: self.runCamera)
            }
        } catch {
            print(error)
            throw error
        }
        return dcd!.getEndpointId()
    }
    
    private func stopDevice() {
        if self.dcd != nil {
            do {
                try dcd!.close()
            } catch {
                print("Issue while closing Directly Connected Device.")
            }
            dcd = nil
        }
    }
    
    func createVirtualDevice(endpointId:String, callback: @escaping (ClientError?) -> ())  {
        do {
            try self.dcd!.getDeviceModel(deviceModelUrn: MOTION_ACTIVATED_CAMERA_MODEL_URN,
                                         callback: { (deviceModel, error) in
                if let error = error {
                    callback(error)
                    return
                }
                do {
                    self.virtualMAC =
                        try self.dcd!.createVirtualDevice(deviceId: endpointId,
                                                    deviceModel: deviceModel!)
                } catch  {
                    print(error)
                    callback(error as? ClientError)
                    return
                }
                // Run the camera in a separate thread not in the the current
                // call satck or getDeviceModel
                DispatchQueue(label: "RunCamera Queue", qos: .utility).async(
                    execute: { callback(nil) })
            })
        } catch {
            print(error)
            callback(error as? ClientError)
        }
    }

    func onError(event:ErrorEvent) {
        let device:VirtualDevice =  event.getVirtualDevice()
        updateGUI(nil, ISO8601DateFormatter().string(from: Date()) +
            " : onError : " + device.getEndpointId() +
            " : \"" + event.getMessage() + "\"")
    }

    private func getImagesDir() -> String {
        return (Bundle.main.url(forResource: "images",
                                withExtension: nil)?.path)!
    }
    
    private func getVideosDir() -> String {
        return (Bundle.main.url(forResource: "videos",
                                withExtension: nil)?.path)!
    }
    
    private func getFiles(dir:String) -> [String] {
        do {
            let dircontents = try FileManager.default.contentsOfDirectory(atPath: dir)
            let sortedArray = dircontents.sorted {
                $0.compare($1, options: .numeric) == .orderedAscending
            }
            return sortedArray
        } catch {
            return []
        }
    }
    
    private func startPhotoCapture() {
        imageIndex = 0
        // Loop to capture photos and update dewvice
        DispatchQueue.main.async(execute: {
            self.capturePhotoTimer =
                Timer.scheduledTimer(timeInterval: self.captureInterval,
                    target: self,
                    selector: #selector(MotionActivatedCamera.capturePhoto),
                    userInfo: nil, repeats: true)
        })
    }
    
    @objc
    private func capturePhoto() {
        let sonameformat = "motion_activated_camera_%@"
        let msgformat = "%@ : Set : \"image\"=%@"
        
        let dir = getImagesDir()
        let images = getFiles(dir: dir)
        if images.isEmpty || images.count < imageIndex % images.count {
            print ("no images")
            return
        }
        var imageName = images[imageIndex % images.count]
        imageIndex += 1
        // Sort of assumes the images are in the "imageIndex" order
        let imagePath = dir + "/" + imageName
            
        // Make the top level file name unique with the endpoint id when
        // not adding the prefix
        if UNIQUE_FILENAME {
            imageName = (virtualMAC?.getEndpointId() ?? "id_unknown") + "_" + imageName
        }

        // May not want full path, probably just file name.
        let msg = String(format: msgformat,
                         formatDate(date:Date()), imagePath)

        do {
            /*
             * The name of video will be automatically
             * prefixed with the device's client id and "/"
             */
            let storageObject:StorageObject = try dcd!.createStorageObject(
                    name: String(format: sonameformat, imageName),
                    contentType:"image/jpeg")
            
            storageObject.setOnSync(callback: syncCallback)
            try storageObject.setInputPath(inputPath: imagePath)
            
            // TODO: Need to resolve issues with DATETIME attributes
            // It doesn't appear to be consistent throughout the library.
            // In this case it needs to "NSNumber"
            try virtualMAC?.update()
                .set(attributeName: IMAGE_ATTRIBUTE,
                     attributeValue: storageObject)
                .set(attributeName: IMAGE_TIME_ATTRIBUTE,
                     attributeValue: Int64(Date().timeIntervalSince1970) as AnyObject)
                .finish();
            print(msg)
            self.updateGUI(nil, msg)
        } catch {
            print (error)
        }
        
    }

    // JavaSE sample
    // assumes videos.length > 0 and videos are 15 second increments.
    // the video files are indexed by interval 0-15, 16-31,
    // ... videos[Math.min(duration/15-1,videos.length-1)];
    
    private func record(virtualDevice: VirtualDevice, data: AnyObject?) {
        guard let d = data else {
            return
        }
        
        let n = Int(truncating: d as! NSNumber)
        if n <= 0 {
            return
        }
        let startDate:Date = Date()
        let msg = String(format:"%@ : Call : record : %d seconds",
                        formatDate(date:startDate), n)
        print(msg)
        self.updateGUI(nil, msg)
        
        // round to nearest increment of 15
        let duration = (n + 14) / 15 * 15
        // assumes videos.length > 0 and videos are 15 second increments.
        let dir = getVideosDir()
        let videos = getFiles(dir: dir)
        var videoName = videos[min(duration/15-1, videos.count-1)]
        let videoFile = dir + "/" + videoName
        
        // Make the top level file name unique with the endpoint id when
        // not adding the prefix
        if UNIQUE_FILENAME {
            videoName = virtualDevice.getEndpointId() + "_" + videoName
        }
        let format = "motion_activated_camera_%@"
        
        // Simulate recording for duration
        DispatchQueue(label: "recording queue", qos: .utility).asyncAfter(
            deadline: DispatchTime.now() + .seconds(n),
            execute: DispatchWorkItem(qos: .utility, block: {
                do {
                    let storageObject:StorageObject  = try (self.dcd?.createStorageObject(
                        name: String(format: format, videoName),
                        contentType: "video/mp4"))!

                    storageObject.setOnSync(callback: self.syncCallback)
                    try storageObject.setInputPath(inputPath: videoFile)
                    
                    let msg = String(format: "%@ : Set : recording = %@",
                                     self.formatDate(date:Date()),
                                     storageObject.getURI())
                    
                    let recording:DeviceLib.Data = try virtualDevice.createData(
                        format: MOTION_ACTIVATED_CAMERA_MODEL_URN + ":recording");
                    try recording.set(field: "video", value: storageObject)
                        .set(field: "startTime", value: Int64(startDate.timeIntervalSince1970) as AnyObject)
                        .set(field: "duration", value: duration as AnyObject)
                        .submit()
                    
                    print(msg)
                    self.updateGUI(nil, msg)
                    
                } catch {
                    print(error)
                }
            }))
    }
    
    private let dateFormatter = DateFormatter()
    private func formatDate(date:Date) -> String {
        dateFormatter.timeStyle = .medium
        dateFormatter.dateStyle = .medium
        return dateFormatter.string(from: date)
    }
}
