#### libremiliacr
#### Copyright(C) 2020-2024 Remilia Scarlet <remilia@posteo.jp>
####
#### This program is free software: you can redistribute it and/or modify
#### it under the terms of the GNU General Public License as published
#### the Free Software Foundation, either version 3 of the License, or
#### (at your option) any later version.
####
#### This program is distributed in the hope that it will be useful,
#### but WITHOUT ANY WARRANTY; without even the implied warranty of
#### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
#### GNU General Public License for more details.
####
#### You should have received a copy of the GNU General Public License
#### along with this program.If not, see<http:####www.gnu.org/licenses/.>
require "./remilib/*"
require "./remilib/math/*"
require "./remilib/args/*"
require "./remilib/console/*"
require "./remilib/compression/bzip/*"
require "./remilib/digest/*"
require "./remilib/config/*"

lib LibC
  fun remi_mkdtemp = "mkdtemp"(template : Pointer(LibC::Char)) : Pointer(LibC::Char)
end

# libremiliacr is a small library that provides utility functions and some extra
# batteries to [Crystal](https://crystal-lang.org/).  It's similar in nature
# (and design) to my
# [cl-sdm](https://chiselapp.com/user/MistressRemilia/repository/cl-sdm/)
# library for Common Lisp.
module RemiLib
  # The version of the library.
  VERSION = "0.92.6"

  # Raised when `#assert` fails.
  class AssertionError < Exception
  end

  # Unless `-Dremilib_no_assertions` is passed to the compiler, this evalutes
  # `expr`.  If it evalutes to `true`, then an `AssertionError` is raised,
  # otherwise this does nothing.
  #
  # If `-Dremilib_no_assertions` is **NOT** passed to the compiler, then this is
  # effectively a no-op.
  macro assert(expr)
    {% unless flag?(:remilib_no_assertions) %}
      unless {{expr}}
        raise ::RemiLib::AssertionError.new("Assertion failure: #{ {{expr.stringify}} }")
      end
    {% end %}
  end

  # Unless `-Dremilib_no_assertions` is passed to the compiler, this evalutes
  # `expr`.  If it evalutes to `true`, then an `AssertionError` is raised with
  # `msg` as a custom error message, otherwise this does nothing.
  #
  # If `-Dremilib_no_assertions` is **NOT** passed to the compiler, then this is
  # effectively a no-op.
  macro assert(msg, expr)
    {% unless flag?(:remilib_no_assertions) %}
      unless {{expr}}
        raise ::RemiLib::AssertionError.new({{msg}})
      end
    {% end %}
  end

  # Defines raise-on-nil and nilable getter methods for each of the given
  # arguments, as well as a setter method that can only be called if the current
  # value is `nil`.
  macro setonce!(*names)
    {% for name in names %}
      {% if name.is_a?(TypeDeclaration) %}
        @{{name}}?

        def {{name.var.id}}? : {{name.type}}?
          @{{name.var.id}}
        end

        def {{name.var.id}} : {{name.type}}
          if (value = @{{name.var.id}}).nil?
            ::raise NilAssertionError.new("#{{{@type}}}#{'#'}{{name.var.id}} cannot be nil")
          else
            value
          end
        end

        def {{name.var.id}}=(newThing : {{name.type}})
          unless @{{name.var.id}}.nil?
            raise "Cannot set {{@type}}#{'#'}{{name.id}} twice"
          else
            @{{name.var.id}} = newThing
          end
        end
      {% else %}
        def {{name.id}}?
          @{{name.id}}
        end

        def {{name.id}}
          if (value = @{{name.id}}).nil?
            ::raise NilAssertionError.new("{{@type}}#{'#'}{{name.id}} cannot be nil")
          else
            value
          end
        end

        def {{name.var.id}}=(newThing : {{name.type}})
          unless @{{name.var.id}}.nil?
            raise "Cannot set {{@type}}#{'#'}{{name.id}} twice"
          else
            @{{name.var.id}} = newThing
          end
        end
      {% end %}
    {% end %}
  end

  # Defines raise-on-nil and nilable getter methods for each of the given
  # arguments, as well as a setter method that can only be called if the current
  # value is `nil`.
  macro classSetonce!(*names)
    {% for name in names %}
      {% if name.is_a?(TypeDeclaration) %}
        @@{{name}}?

        def self.{{name.var.id}}? : {{name.type}}?
          @@{{name.var.id}}
        end

        def self.{{name.var.id}} : {{name.type}}
          if (value = @@{{name.var.id}}).nil?
            ::raise NilAssertionError.new("#{{{@type}}}#{'#'}{{name.var.id}} cannot be nil")
          else
            value
          end
        end

        def self.{{name.var.id}}=(newThing : {{name.type}})
          unless @@{{name.var.id}}.nil?
            raise "Cannot set {{@type}}#{'#'}{{name.id}} twice"
          else
            @@{{name.var.id}} = newThing
          end
        end
      {% else %}
        def self.{{name.id}}?
          @@{{name.id}}
        end

        def self.{{name.id}}
          if (value = @@{{name.id}}).nil?
            ::raise NilAssertionError.new("{{@type}}#{'#'}{{name.id}} cannot be nil")
          else
            value
          end
        end

        def self.{{name.var.id}}=(newThing : {{name.type}})
          unless @@{{name.var.id}}.nil?
            raise "Cannot set {{@type}}#{'#'}{{name.id}} twice"
          else
            @@{{name.var.id}} = newThing
          end
        end
      {% end %}
    {% end %}
  end

  # Creates a "displaced array", which is a `Slice` that has no storage
  # of its own, but instead points into a subsequence within
  # `displacedTo`.  Changes to the yielded slice will change
  # `displacedTo`.
  #
  # The yielded `Slice`'s index 0 will correspond to
  # `displacedTo[displacedIndexOffset]`, while `size` is the number of
  # elements in `Slice`.  The total size of the yielded `Slice` must
  # be less than or equal to the size of `displacedTo`.
  #
  # http://www.lispworks.com/documentation/HyperSpec/Body/26_glo_d.htm#displaced_array
  def self.withDisplacedArray(displacedTo : Array(T)|Slice(T), displacedIndexOffset : Int, size : Int, &) forall T
    if displacedIndexOffset + size > displacedTo.size
      raise "Displaced array is too small"
    end
    displaced = Slice.new(displacedTo.to_unsafe + displacedIndexOffset, size)
    yield displaced
  end

  class MkdtempError < Exception
    getter errno : Errno?

    def initialize(message : String)
      super(message, nil)
    end

    def initialize(message : String, cause : Exception|Nil)
      super(message, cause)
    end

    def initialize(message : String|Nil, cause : Exception|Nil, @errno : Errno|Nil)
      super(message, cause)
    end
  end

  # Creates a temporary directory using `mkdtemp()` from C's standard lib.  The
  # directory is created with its permissions set to 0700.  On failure, this
  # will raise a `MkdtempError` exception.
  def self.mkdtemp(prefix : String = "") : String
    template = String.build do |str|
      str << prefix
      str << "XXXXXX"
    end
    path = Path[Dir.tempdir, template]
    templatePtr = path.to_s.bytes.dup # dup may not be needed here

    Errno.value = Errno::NONE
    str = LibC.remi_mkdtemp(templatePtr.to_unsafe)

    if str.null?
      raise MkdtempError.new("Could not create temporary directory")
    elsif Errno.value.einval?
      raise MkdtempError.new("Bad mkdtemp template", nil, Errno.value)
    elsif !Errno.value.none?
      raise MkdtempError.new("Could not create temporary directory", nil, errno: Errno.value)
    end

    String.new(str)
  end
end
