//===----------------------------------------------------------------------===//
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//

@frozen
public struct IPv4Address: Sendable, Hashable, CustomStringConvertible, Equatable, Comparable {
    public let value: UInt32

    /// Creates an IPv4Address from an unsigned integer.
    ///
    /// - Parameter string: The integer representation of the address.
    @inlinable
    public init(_ value: UInt32) {
        self.value = value
    }

    /// Creates an IPv4Address from 4 bytes.
    ///
    /// - Parameters:
    ///   - bytes: 4-byte array in network byte order representing the IPv4 address
    /// - Throws: `AddressError.unableToParse` if the byte array length is not 4
    @inlinable
    public init(_ bytes: [UInt8]) throws {
        guard bytes.count == 4 else {
            throw AddressError.unableToParse
        }
        self.value =
            (UInt32(bytes[0]) << 24)
            | (UInt32(bytes[1]) << 16)
            | (UInt32(bytes[2]) << 8)
            | UInt32(bytes[3])
    }

    /// Creates an IPv4Address from a string representation.
    ///
    /// - Parameter string: The IPv4 address string in dotted decimal notation (e.g., "192.168.1.1")
    /// - Throws: `AddressError.unableToParse` if the string is not a valid IPv4 address
    @inlinable
    public init(_ string: String) throws {
        self.value = try Self.parse(string)
    }

    @inlinable
    public var bytes: [UInt8] {
        Self.bytes(value)
    }

    @usableFromInline
    static func bytes(_ value: UInt32) -> [UInt8] {
        var result = [UInt8](repeating: 0, count: 4)
        result[0] = UInt8((value >> 24) & 0xff)
        result[1] = UInt8((value >> 16) & 0xff)
        result[2] = UInt8((value >> 8) & 0xff)
        result[3] = UInt8(value & 0xff)
        return result
    }

    // TODO: spans?
    @available(macOS 26.0, *)
    @usableFromInline
    static func bytes(_ value: UInt32) -> InlineArray<4, UInt8> {
        let result: InlineArray<4, UInt8> = [
            UInt8((value >> 24) & 0xff),
            UInt8((value >> 16) & 0xff),
            UInt8((value >> 8) & 0xff),
            UInt8(value & 0xff),
        ]
        return result
    }

    @inlinable
    public var description: String {
        "\(bytes[0]).\(bytes[1]).\(bytes[2]).\(bytes[3])"
    }

    /// Parses an IPv4 address string in dotted decimal notation into a UInt32 representation.
    ///
    /// ## Validation Rules
    /// - Exactly 4 octets separated by dots
    /// - Each octet must be 0-255
    /// - No leading zeros (except for "0" itself)
    /// - No whitespace characters
    /// - Only digits and dots allowed
    ///
    /// ## Examples
    /// ```swift
    /// IPv4Address.parse("192.168.1.1")    // Returns: 3232235777
    /// IPv4Address.parse("127.0.0.1")      // Returns: 2130706433
    /// IPv4Address.parse("0.0.0.0")        // Returns: 0
    /// IPv4Address.parse("255.255.255.255") // Returns: 4294967295
    ///
    /// // Invalid examples:
    /// IPv4Address.parse("192.168.1")       // Wrong number of octets
    /// IPv4Address.parse("192.168.1.256")   // Octet out of range
    /// IPv4Address.parse("192.168.001.1")   // Leading zeros
    /// IPv4Address.parse(" 192.168.1.1 ")   // Whitespace
    /// ```
    ///
    /// - Parameter s: The IPv4 address string to parse
    /// - Returns: The 32-bit representation of the IP address, or `nil` if parsing fails
    /// - Note: The returned value is in network byte order (big-endian)
    @usableFromInline
    internal static func parse(_ s: String) throws -> UInt32 {
        guard !s.isEmpty, s.count >= 7, s.count <= 15 else {
            throw AddressError.unableToParse
        }

        // IP addresses should only contain ASCII digits and dots
        let utf8 = s.utf8
        for byte in utf8 {
            // ASCII whitespace: space(32), tab(9), newline(10), return(13)
            if byte == 32 || byte == 9 || byte == 10 || byte == 13 {
                throw AddressError.unableToParse
            }
        }

        // accumulator for the 32bit representation of the IPv4 address
        var result: UInt32 = 0

        // tracking octet count, max 4 allowed
        var octetCount = 0
        var currentOctet = 0

        // number of digits in the string representation of the octet, max 3
        var digitCount = 0

        for byte in utf8 {
            if byte == 46 {  // ASCII '.'
                // Validate octet before processing
                guard octetCount < 3, digitCount > 0, digitCount <= 3, currentOctet <= 255 else {
                    throw AddressError.unableToParse
                }

                // Shift result and add current octet
                result = (result << 8) | UInt32(currentOctet)

                // Reset for next octet
                octetCount += 1
                currentOctet = 0
                digitCount = 0

            } else if byte >= 48 && byte <= 57 {  // ASCII '0'-'9'
                let digit = Int(byte - 48)

                digitCount += 1

                // Check for invalid leading zeros: "01", "001", etc.
                // Allow single "0" but reject multi-digit numbers starting with 0
                if digitCount == 1 && digit == 0 {
                    // First digit is 0 - this is only valid if it's the only digit
                    currentOctet = 0
                } else if digitCount > 1 && currentOctet == 0 {
                    // We had a leading zero and now have more digits - invalid
                    throw AddressError.unableToParse
                } else {
                    // Normal case: build the octet value
                    currentOctet = currentOctet * 10 + digit
                }

                // Early termination if octet becomes too large
                guard currentOctet <= 255, digitCount <= 3 else {
                    throw AddressError.unableToParse
                }

            } else {
                throw AddressError.unableToParse
            }
        }

        // Validate final octet
        guard octetCount == 3, digitCount > 0, digitCount <= 3, currentOctet <= 255 else {
            throw AddressError.unableToParse
        }

        return (result << 8) | UInt32(currentOctet)
    }

    // MARK: - Address Classification Methods

    /// Returns `true` if this is the unspecified address (0.0.0.0).
    ///
    /// Per RFC 791, 0.0.0.0 is the "this network" address.
    @inlinable
    public var isUnspecified: Bool {
        value == 0
    }

    /// Returns `true` if this is a loopback address (127.0.0.0/8).
    ///
    /// Per RFC 1122 Section 3.2.1.3, the entire 127.0.0.0/8 block is reserved for loopback.
    @inlinable
    public var isLoopback: Bool {
        (value & 0xFF00_0000) == 0x7F00_0000
    }

    /// Returns `true` if this is a multicast address (224.0.0.0/4).
    ///
    /// Per RFC 1112, addresses in the range 224.0.0.0 to 239.255.255.255 are multicast addresses.
    @inlinable
    public var isMulticast: Bool {
        (value & 0xF000_0000) == 0xE000_0000
    }

    /// Returns `true` if this is a link-local address (169.254.0.0/16).
    ///
    /// Per RFC 3927, 169.254.0.0/16 is reserved for link-local addresses (APIPA/Auto-IP).
    @inlinable
    public var isLinkLocal: Bool {
        (value & 0xFFFF_0000) == 0xA9FE_0000
    }

    /// Returns `true` if this is the limited broadcast address (255.255.255.255).
    ///
    /// Per RFC 919/922, 255.255.255.255 is the limited broadcast address.
    @inlinable
    public var isBroadcast: Bool {
        value == 0xFFFF_FFFF
    }

    /// Compares two IPv4 addresses numerically.
    @inlinable
    public static func < (lhs: IPv4Address, rhs: IPv4Address) -> Bool {
        lhs.value < rhs.value
    }
}

extension IPv4Address: Codable {
    public init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let string = try container.decode(String.self)
        try self.init(string)
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(description)
    }
}
