/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import Foundation

/// The reasons a  ping may be sent.
/// Reason codes can be of any type, but need to adhere to a protocol.
///
/// For user-defined custom pings associated reason codes will be defined as `enums`.
public protocol ReasonCodes: Hashable {
    /// The index of the reason code, used to index the string array passed at
    /// `Ping` instantiation.
    func index() -> Int
}

/// Default of no reason codes for pings.
///
/// An enum with no values for convenient use as the default set of reason codes
/// that an `Ping` can accept.
public enum NoReasonCodes: ReasonCodes {
    public func index() -> Int {
        return 0
    }
}

/// A base class for functionality across all Ping<T>
public class PingBase {
    var handle: UInt64
    var name: String

    /// Create a new Ping with the given name.
    public init(name: String) {
        self.name = name
        self.handle = 0
    }
}

/// This implements the developer facing API for custom pings.
///
/// Instances of this class type are automatically generated by the parsers at build time.
///
/// The Ping API only exposes the `Ping.sumbit()` method, which collects and
/// schedules a ping for eventual upload.
public class Ping<ReasonCodesEnum: ReasonCodes>: PingBase {
    let includeClientId: Bool
    let reasonCodes: [String]
    var testCallback: ((ReasonCodesEnum?) throws -> Void)?

    /// The public constructor used by automatically generated metrics.
    public init(name: String, includeClientId: Bool, sendIfEmpty: Bool, reasonCodes: [String]) {
        self.includeClientId = includeClientId
        self.reasonCodes = reasonCodes
        super.init(name: name)
        self.handle = withArrayOfCStrings(reasonCodes) { reasonCodesArray in
            glean_new_ping_type(
                name,
                includeClientId.toByte(),
                sendIfEmpty.toByte(),
                reasonCodesArray,
                Int32(reasonCodes.count)
            )
        }
        NSLog("Registering this ping: \(name)")
        Glean.shared.registerPingType(self)
    }

    /// Destroy this ping type.
    deinit {
        if self.handle != 0 {
            glean_destroy_ping_type(self.handle)
        }
    }

    /// **Test-only API**
    ///
    /// Attach a callback to be called right before a new ping is submitted.
    /// The provided function is called exactly once before submitting a ping.
    ///
    /// Note: The callback will be called on any call to submit.
    /// A ping might not be sent afterwards, e.g. if the ping is otherwise empty (and
    /// `send_if_empty` is `false`).
    public func testBeforeNextSubmit(cb: @escaping (ReasonCodesEnum?) throws -> Void) {
        Dispatchers.shared.assertInTestingMode()
        self.testCallback = cb
    }

    /// Collect and submit the ping for eventual uploading.
    ///
    /// While the collection of metrics into pings happens synchronously, the
    /// ping queuing and ping uploading happens asyncronously.
    /// There are no guarantees that this will happen immediately.
    ///
    /// If the ping currently contains no content, it will not be queued.
    ///
    /// - parameters:
    ///     * reason: The reason the ping is being submitted.
    public func submit(reason: ReasonCodesEnum? = nil) {
        if let cb = self.testCallback {
            do {
                try cb(reason)
            } catch {
                assert(false, "Callback threw before submitting ping \(name).")
            }
            self.testCallback = nil
        }

        var reasonString: String?
        if reason != nil {
            reasonString = self.reasonCodes[reason!.index()]
        }
        Glean.shared.submitPing(self, reason: reasonString)
    }

    /// Collect and submit the ping for eventual uploading.
    ///
    /// **THIS METHOD IS DEPRECATED.**  Use `submit` instead.
    ///
    /// While the collection of metrics into pings happens synchronously, the
    /// ping queuing and ping uploading happens asyncronously.
    /// There are no guarantees that this will happen immediately.
    ///
    /// If the ping currently contains no content, it will not be queued.
    @available(*, deprecated, renamed: "submit")
    public func send() {
        self.submit()
    }
}
