<?php
if (!defined('DBASE_TYPE_DBASE')) {
  define('DBASE_TYPE_DBASE', 3);
}
if (!defined('DBASE_TYPE_FOXPRO')) {
  define('DBASE_TYPE_FOXPRO', 48);
}

if (!function_exists('_dbase_sdn_to_gregorian')) {
  function _dbase_sdn_to_gregorian($sdn) {
    if ($sdn < 1721426 || $sdn > 5373484) {
      return array(0, 0, 0);
    }
    $temp = (int) (($sdn + 32045) * 4 - 1);
    $century = floor($temp / 146097);
    $temp = floor(($temp % 146097) / 4) * 4 + 3;
    $year = ($century * 100) + floor($temp / 1461);
    $dayOfYear = floor(($temp % 1461) / 4) + 1;
    $temp = $dayOfYear * 5 - 3;
    $month = floor($temp / 153);
    $day = floor(($temp % 153) / 5) + 1;
    if ($month < 10) {
      $month += 3;
    } else {
      $year += 1;
      $month -= 9;
    }
    $year -= 4800;
    if ($year <= 0) {
      $year--;
    }
    return array($year, $month, $day);
  }
}

if (!function_exists('_dbase_gregorian_to_sdn')) {
  function _dbase_gregorian_to_sdn($inputYear, $inputMonth, $inputDay) {
    if ($inputYear < 1 || $inputYear > 9999 || $inputMonth < 1 || $inputMonth > 12 || $inputDay < 1 || $inputDay > 31) {
      return 0;
    }
    $year = $inputYear + 4800;
    if ($inputMonth > 2) {
      $month = $inputMonth - 3;
    } else {
      $month = $inputMonth + 9;
      $year--;
    }
    return floor((floor($year / 100) * 146097) / 4) + floor(($year % 100) * 1461 / 4) + floor(($month * 153 + 2) / 5) + $inputDay - 32045;
  }
}

if (!function_exists('_dbase_get_timestamp')) {
  function _dbase_get_timestamp($val) {
    if (!preg_match('/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\.(\d{3})$/', $val, $m)) {
      return pack('VV', 0, 0);
    }
    $jdn = _dbase_gregorian_to_sdn((int)$m[1], (int)$m[2], (int)$m[3]);
    $msecs = ((int)$m[4] * 3600 + (int)$m[5] * 60 + (int)$m[6]) * 1000 + (int)$m[7];
    return pack('VV', $jdn, $msecs);
  }
}

if (!function_exists('_dbase_set_timestamp')) {
  function _dbase_set_timestamp($bin) {
    if (strlen($bin) < 8) {
      return '';
    }
    $u = unpack('Vjdn/Vmsecs', $bin);
    $jdn = unpack('l', pack('V', $u['jdn']))[1];
    $msecs = unpack('l', pack('V', $u['msecs']))[1];
    if ($jdn == 0 && $msecs == 0) {
      return '00000000000000.000';
    }
    list($year, $month, $day) = _dbase_sdn_to_gregorian($jdn);
    $millis = $msecs % 1000;
    $msecs = floor($msecs / 1000);
    $second = $msecs % 60;
    $msecs = floor($msecs / 60);
    $minute = $msecs % 60;
    $msecs = floor($msecs / 60);
    $hour = $msecs;
    return sprintf('%04d%02d%02d%02d%02d%02d.%03d', $year, $month, $day, $hour, $minute, $second, $millis);
  }
}

if (!function_exists('_dbase_update_date')) {
  function _dbase_update_date($fp) {
    $date = getdate();
    $pos = ftell($fp);
    fseek($fp, 1, SEEK_SET);
    fwrite($fp, pack('CCC', $date['year'] - 1900, $date['mon'], $date['mday']));
    fseek($fp, $pos, SEEK_SET);
  }
}

if (!function_exists('_dbase_arg_error')) {
  function _dbase_arg_error($func, $expected, $given, $at_least = false) {
    $s = $expected > 1 ? 's' : '';
    $exact = $at_least ? 'at least' : 'exactly';
    if (PHP_VERSION_ID >= 80000) {
      throw new ArgumentCountError("$func() expects $exact $expected argument$s, $given given");
    } else {
      trigger_error("$func() expects $exact $expected parameter$s, $given given", E_USER_WARNING);
    }
  }
}

if (!function_exists('_dbase_type_error')) {
  function _dbase_type_error($func, $argNum, $argName, $expectedType, $givenType) {
    if (PHP_VERSION_ID >= 80000) {
      if ($givenType === 'boolean') $givenType = 'bool';
      if ($givenType === 'integer') $givenType = 'int';
      if ($givenType === 'double') $givenType = 'float';
      throw new TypeError("$func(): Argument #$argNum (\$$argName) must be of type $expectedType, $givenType given");
    } else {
      trigger_error("$func() expects parameter $argNum to be $expectedType, $givenType given", E_USER_WARNING);
    }
  }
}

class DbasePolyfillState {
  public static $databases = array();

  public static function add($resource, $meta) {
    $id = (int)$resource;
    self::$databases[$id] = $meta;
  }
  public static function remove($resource) {
    $id = (int)$resource;
    unset(self::$databases[$id]);
  }
  public static function get($resource) {
    if (!is_resource($resource)) {
      return null;
    }
    $id = (int)$resource;
    return self::$databases[$id] ?? null;
  }
  public static function update($resource, $meta) {
    $id = (int)$resource;
    if (isset(self::$databases[$id])) {
      self::$databases[$id] = $meta;
    }
  }
}

if (!function_exists('dbase_create')) {
  function dbase_create() {
    $num_args = func_num_args();
    if ($num_args < 2 || $num_args > 3) {
      if ($num_args < 2) _dbase_arg_error('dbase_create', 2, $num_args, true);
      else _dbase_arg_error('dbase_create', 3, $num_args, false);
      return null;
    }
    $args = func_get_args();
    $path = $args[0];
    $fields = $args[1];
    $type = $num_args > 2 ? $args[2] : DBASE_TYPE_DBASE;

    if ($type !== DBASE_TYPE_DBASE && $type !== DBASE_TYPE_FOXPRO) {
      trigger_error("dbase_create(): unknown database type $type", E_USER_WARNING);
      return false;
    }

    if (!is_string($path)) {
      _dbase_type_error('dbase_create', 1, 'path', 'string', gettype($path));
      return null;
    }
    if (!is_array($fields)) {
      _dbase_type_error('dbase_create', 2, 'fields', 'array', gettype($fields));
      return null;
    }

    if (empty($fields)) {
      trigger_error('dbase_create(): Unable to create database without fields', E_USER_WARNING);
      return false;
    }

    $header = "\x03"; // dBase III without memo
    if ($type === DBASE_TYPE_FOXPRO) {
      $header = "\x30"; // Visual FoxPro
    }

    $date = getdate();
    $header .= pack('CCC', $date['year'] - 1900, $date['mon'], $date['mday']);

    $record_length = 1;
    $field_descriptors = '';
    $parsed_fields = array();

    foreach ($fields as $i => $field) {
      if (!is_array($field)) {
        trigger_error("dbase_create(): expected field name as element 0 of list", E_USER_WARNING);
        return false;
      }
      if (!array_key_exists(0, $field) || (!is_string($field[0]) && !is_object($field[0]) && !is_numeric($field[0]))) {
        trigger_error("dbase_create(): expected field name as element 0 of list in field $i", E_USER_WARNING);
        return false;
      }
      $name = (string)$field[0];
      if ($name === '' || strlen($name) > 10) {
        trigger_error("dbase_create(): invalid field name '$name' (must be non-empty and less than or equal to 10 characters) in field $i", E_USER_WARNING);
        return false;
      }

      if (!array_key_exists(1, $field) || (!is_string($field[1]) && !is_object($field[1]) && !is_numeric($field[1]))) {
        trigger_error("dbase_create(): expected field type as element 1 of list in field $i", E_USER_WARNING);
        return false;
      }
      $f_type_str = (string)$field[1];
      if (strlen($f_type_str) !== 1) {
        trigger_error("dbase_create(): expected field type as element 1 of list in field $i", E_USER_WARNING);
        return false;
      }
      $f_type = strtoupper($f_type_str[0]);

      if (!in_array($f_type, array('C', 'D', 'N', 'L', 'M', 'F', 'B', 'G', 'P', 'Y', 'T', 'I', 'V', 'X', '@', 'O', '+'))) {
        trigger_error("dbase_create(): unknown field type '$f_type' in field $i", E_USER_WARNING);
        return false;
      }

      if ($f_type === 'T' && $type === DBASE_TYPE_DBASE) {
        trigger_error("dbase_create(): datetime fields are not supported by dBASE", E_USER_WARNING);
        return false;
      }

      if (!array_key_exists(2, $field)) {
        trigger_error("dbase_create(): expected field length as element 2 of list in field $i", E_USER_WARNING);
        return false;
      }
      $length = (int)$field[2];
      if ($length < 0 || $length > 254) {
        trigger_error("dbase_create(): expected length of field $i to be in range 0..254, but got $length", E_USER_WARNING);
        return false;
      }

      $precision = 0;
      if ($f_type === 'N' || $f_type === 'F') {
        if (!array_key_exists(3, $field)) {
          trigger_error("dbase_create(): expected field precision as element 3 of list in field $i", E_USER_WARNING);
          return false;
        }
        $precision = (int)$field[3];
        if ($precision < 0 || $precision > 254) {
          trigger_error("dbase_create(): expected precision of field $i to be in range 0..254, but got $precision", E_USER_WARNING);
          return false;
        }
      }

      $parsed_fields[] = array(
        'name' => $name,
        'type' => $f_type,
        'length' => $length,
        'precision' => $precision
      );
      $record_length += $length;
      $field_descriptors .= str_pad($name, 11, "\0", STR_PAD_RIGHT).$f_type.pack('xxxxCCxxxxxxxxxxxxxx', $length, $precision);
    }

    $header_length = 32 + (32 * count($parsed_fields)) + 1;
    $header .= pack('Vv2x20', 0, $header_length, $record_length);
    $header[8] = chr($header_length & 0xFF);
    $header[9] = chr(($header_length >> 8) & 0xFF);
    $header[10] = chr($record_length & 0xFF);
    $header[11] = chr(($record_length >> 8) & 0xFF);

    $fp = @fopen($path, 'w+b');
    if (!$fp) {
      trigger_error("dbase_create(): unable to open database $path", E_USER_WARNING);
      return false;
    }

    fwrite($fp, $header);
    fwrite($fp, $field_descriptors);
    fwrite($fp, "\x0D");
    if ($type === DBASE_TYPE_FOXPRO) {
      fwrite($fp, str_repeat("\x00", 263));
    }
    fwrite($fp, "\x1A");
    fclose($fp);

    return dbase_open($path, 2);
  }
}

if (!function_exists('dbase_open')) {
  function dbase_open() {
    $num_args = func_num_args();
    if ($num_args != 2) {
      _dbase_arg_error('dbase_open', 2, $num_args);
      return null;
    }
    $args = func_get_args();
    $filename = $args[0];
    $mode = $args[1];

    if (!is_string($filename)) {
      _dbase_type_error('dbase_open', 1, 'path', 'string', gettype($filename));
      return null;
    }
    if (!is_int($mode)) {
      _dbase_type_error('dbase_open', 2, 'mode', 'int', gettype($mode));
      return null;
    }

    if ($mode !== 0 && $mode !== 1 && $mode !== 2) {
      trigger_error("dbase_open(): Invalid access mode $mode", E_USER_WARNING);
      return false;
    }

    if ($filename === '') {
      trigger_error("dbase_open(): The filename cannot be empty.", E_USER_WARNING);
      return false;
    }

    if ($mode === 1) {
      trigger_error("dbase_open(): Cannot open $filename in write-only mode", E_USER_WARNING);
      return false;
    }

    $modes = array('rb', 'r+b', 'r+b');
    $fp = @fopen($filename, $modes[$mode]);
    if (!$fp) {
      trigger_error('dbase_open(): unable to open database '.$filename, E_USER_WARNING);
      return false;
    }

    $header = fread($fp, 32);
    if (strlen($header) < 32) {
      trigger_error('dbase_open(): unable to open database '.$filename, E_USER_WARNING);
      fclose($fp);
      return false;
    }

    $h = unpack('Ctype/Cyear/Cmonth/Cday/Vrecords/vheader_length/vrecord_length', substr($header, 0, 12));

    $num_fields = ($h['header_length'] - 32 - 1) / 32;
    if (!is_int($num_fields) && floor($num_fields) != $num_fields) {
      $num_fields = ($h['header_length'] - 32 - 1 - 263) / 32;
    }
    $num_fields = (int)floor($num_fields);

    $fields = array();
    $offset = 1;
    for ($i=0; $i<$num_fields; $i++) {
      $f_data = fread($fp, 32);
      $f_type = $f_data[11];
      $f_length = ord($f_data[16]);
      $f_precision = ord($f_data[17]);

      if (!in_array($f_type, array('C', 'D', 'N', 'L', 'M', 'F', 'B', 'G', 'P', 'Y', 'T', 'I', 'V', 'X', '@', 'O', '+'))) {
        if (ctype_alpha($f_type)) {
          trigger_error("dbase_open(): unknown field type '$f_type'", E_USER_WARNING);
        } else {
          trigger_error("dbase_open(): unexpected field type '$f_type'", E_USER_WARNING);
        }
        trigger_error("dbase_open(): unable to open database $path", E_USER_WARNING);
        return false;
      }

      $fields[] = array(
        'name' => trim(substr($f_data, 0, 11), "\0"),
        'type' => $f_type,
        'length' => $f_length,
        'precision' => $f_precision,
        'offset' => $offset
      );
      $offset += $f_length;
    }

    DbasePolyfillState::add($fp, array(
      'type' => $h['type'],
      'records' => $h['records'],
      'header_length' => $h['header_length'],
      'record_length' => $h['record_length'],
      'fields' => $fields,
      'path' => $filename,
      'mode' => $mode
    ));

    return $fp;
  }
}

if (!function_exists('dbase_close')) {
  function dbase_close() {
    $num_args = func_num_args();
    if ($num_args != 1) {
      _dbase_arg_error('dbase_close', 1, $num_args);
      return null;
    }
    $database = func_get_arg(0);

    $meta = DbasePolyfillState::get($database);
    if (!$meta) {
      if (PHP_VERSION_ID >= 80000) {
        if (!is_resource($database)) {
          _dbase_type_error('dbase_close', 1, 'database', 'resource', gettype($database));
          return null;
        }
        throw new TypeError('dbase_close(): supplied resource is not a valid dbase resource');
      } else {
        trigger_error('dbase_close() expects parameter 1 to be resource, ' . gettype($database) . ' given', E_USER_WARNING);
      }
      trigger_error('dbase_close(): supplied resource is not a valid dbase resource', E_USER_WARNING);
      return false;
    }

    if (is_resource($database)) {
      if ($meta['mode'] > 0) {
        fseek($database, 4);
        fwrite($database, pack('V', $meta['records']));
      }
      fclose($database);
    }
    DbasePolyfillState::remove($database);
    return true;
  }
}

if (!function_exists('dbase_numrecords')) {
  function dbase_numrecords() {
    $num_args = func_num_args();
    if ($num_args != 1) {
      _dbase_arg_error('dbase_numrecords', 1, $num_args);
      return null;
    }
    $database = func_get_arg(0);
    $meta = DbasePolyfillState::get($database);
    if (!$meta) {
      if (PHP_VERSION_ID >= 80000 && !is_resource($database)) {
        _dbase_type_error('dbase_numrecords', 1, 'database', 'resource', gettype($database));
        return null;
      }
      if (PHP_VERSION_ID < 80000 && !is_resource($database)) {
        trigger_error('dbase_numrecords() expects parameter 1 to be resource, ' . gettype($database) . ' given', E_USER_WARNING);
      }
      if (PHP_VERSION_ID >= 80000) {
        throw new TypeError('dbase_numrecords(): supplied resource is not a valid dbase resource');
      }
      trigger_error('dbase_numrecords(): supplied resource is not a valid dbase resource', E_USER_WARNING);
      return false;
    }
    return $meta['records'];
  }
}

if (!function_exists('dbase_numfields')) {

  function dbase_numfields() {
    $num_args = func_num_args();
    if ($num_args != 1) {
      _dbase_arg_error('dbase_numfields', 1, $num_args);
      return null;
    }
    $database = func_get_arg(0);
    $meta = DbasePolyfillState::get($database);
    if (!$meta) {
      if (PHP_VERSION_ID >= 80000 && !is_resource($database)) {
        _dbase_type_error('dbase_numfields', 1, 'database', 'resource', gettype($database));
        return null;
      }
      if (PHP_VERSION_ID < 80000 && !is_resource($database)) {
        trigger_error('dbase_numfields() expects parameter 1 to be resource, ' . gettype($database) . ' given', E_USER_WARNING);
      }
      if (PHP_VERSION_ID >= 80000) {
        throw new TypeError('dbase_numfields(): supplied resource is not a valid dbase resource');
      }
      trigger_error('dbase_numfields(): supplied resource is not a valid dbase resource', E_USER_WARNING);
      return false;
    }
    return count($meta['fields']);
  }
}

if (!function_exists('dbase_get_header_info')) {
  function dbase_get_header_info() {
    $num_args = func_num_args();
    if ($num_args != 1) {
      _dbase_arg_error('dbase_get_header_info', 1, $num_args);
      return null;
    }
    $database = func_get_arg(0);
    $meta = DbasePolyfillState::get($database);
    if (!$meta) {
      if (PHP_VERSION_ID >= 80000 && !is_resource($database)) {
        _dbase_type_error('dbase_get_header_info', 1, 'database', 'resource', gettype($database));
        return null;
      }
      if (PHP_VERSION_ID < 80000 && !is_resource($database)) {
        trigger_error('dbase_get_header_info() expects parameter 1 to be resource, ' . gettype($database) . ' given', E_USER_WARNING);
      }
      if (PHP_VERSION_ID >= 80000) {
        throw new TypeError('dbase_get_header_info(): supplied resource is not a valid dbase resource');
      }
      trigger_error('dbase_get_header_info(): supplied resource is not a valid dbase resource', E_USER_WARNING);
      return false;
    }
    $res = array();
    foreach ($meta['fields'] as $f) {
      switch (strtoupper($f['type'])) {
        case 'C': $type_name = 'character'; $fmt = '%-'.$f['length'].'s'; break;
        case 'D': $type_name = 'date'; $fmt = '%'.$f['length'].'s'; break;
        case 'N': $type_name = 'number'; $fmt = '%'.$f['length'].'s'; break;
        case 'L': $type_name = 'boolean'; $fmt = '%'.$f['length'].'s'; break;
        case 'M': $type_name = 'memo'; $fmt = '%'.$f['length'].'s'; break;
        case 'F': $type_name = 'float'; $fmt = '%'.$f['length'].'s'; break;
        case 'T': case '@': $type_name = 'datetime'; $fmt = ''; break;
        case 'I': $type_name = 'integer'; $fmt = '%'.$f['length'].'s'; break;
        case 'O': case 'B': $type_name = 'double'; $fmt = '%'.$f['length'].'s'; break;
        case 'Y': $type_name = 'currency'; $fmt = '%'.$f['length'].'s'; break;
        default: $type_name = 'unknown'; $fmt = '%-'.$f['length'].'s';
      }
      $res[] = array(
        'name' => $f['name'],
        'type' => $type_name,
        'length' => $f['length'],
        'precision' => $f['precision'],
        'format' => $fmt,
        'offset' => 0
      );
    }
    $offset = 1;
    foreach ($res as &$r) {
      $r['offset'] = $offset;
      $offset += $r['length'];
    }
    return $res;
  }
}

if (!function_exists('dbase_get_record')) {
  function dbase_get_record() {
    $num_args = func_num_args();
    if ($num_args != 2) {
      _dbase_arg_error('dbase_get_record', 2, $num_args);
      return null;
    }
    $args = func_get_args();
    $database = $args[0];
    $number = $args[1];

    $meta = DbasePolyfillState::get($database);
    if (!$meta) {
      if (PHP_VERSION_ID >= 80000 && !is_resource($database)) {
        _dbase_type_error('dbase_get_record', 1, 'database', 'resource', gettype($database));
        return null;
      }
      if (PHP_VERSION_ID < 80000 && !is_resource($database)) {
        trigger_error('dbase_get_record() expects parameter 1 to be resource, ' . gettype($database) . ' given', E_USER_WARNING);
      }
      if (PHP_VERSION_ID >= 80000) {
        throw new TypeError('dbase_get_record(): supplied resource is not a valid dbase resource');
      }
      trigger_error('dbase_get_record(): supplied resource is not a valid dbase resource', E_USER_WARNING);
      return false;
    }
    if (!is_int($number)) {
      _dbase_type_error('dbase_get_record', 2, 'number', 'int', gettype($number));
      return null;
    }
    if ($number < 1 || $number > $meta['records']) {
      return false;
    }

    fseek($database, $meta['header_length'] + ($number - 1) * $meta['record_length']);
    $data = fread($database, $meta['record_length']);
    if (strlen($data) !== $meta['record_length']) {
      return false;
    }

    $is_deleted = ($data[0] === '*');
    $res = array();
    $pos = 1;
    foreach ($meta['fields'] as $f) {
      $raw_val = substr($data, $pos, $f['length']);
      if ($f['type'] === 'N' || $f['type'] === 'F') {
        $val = trim($raw_val);
        $res[] = strpos($val, '.') !== false ? (float)$val : (int)$val;
      } elseif ($f['type'] === 'L') {
        $val = strtoupper($raw_val[0] ?? ' ');
        if ($val === 'T' || $val === 'Y') {
          $res[] = true;
        } elseif ($val === '?') {
          $res[] = null;
        } else {
          $res[] = false;
        }
      } elseif ($f['type'] === 'T') {
        $res[] = _dbase_set_timestamp($raw_val);
      } else {
        $res[] = $raw_val;
      }
      $pos += $f['length'];
    }
    $res['deleted'] = ($is_deleted ? 1 : 0);
    return $res;
  }
}

if (!function_exists('dbase_get_record_with_names')) {
  function dbase_get_record_with_names() {
    $num_args = func_num_args();
    if ($num_args != 2) {
      _dbase_arg_error('dbase_get_record_with_names', 2, $num_args);
      return null;
    }
    $args = func_get_args();
    $database = $args[0];
    $number = $args[1];

    $meta = DbasePolyfillState::get($database);
    if (!$meta) {
      if (PHP_VERSION_ID >= 80000 && !is_resource($database)) {
        _dbase_type_error('dbase_get_record_with_names', 1, 'database', 'resource', gettype($database));
        return null;
      }
      if (PHP_VERSION_ID < 80000 && !is_resource($database)) {
        trigger_error('dbase_get_record_with_names() expects parameter 1 to be resource, ' . gettype($database) . ' given', E_USER_WARNING);
      }
      if (PHP_VERSION_ID >= 80000) {
        throw new TypeError('dbase_get_record_with_names(): supplied resource is not a valid dbase resource');
      }
      trigger_error('dbase_get_record_with_names(): supplied resource is not a valid dbase resource', E_USER_WARNING);
      return false;
    }
    if (!is_int($number)) {
      _dbase_type_error('dbase_get_record_with_names', 2, 'number', 'int', gettype($number));
      return null;
    }

    fseek($database, $meta['header_length'] + ($number - 1) * $meta['record_length']);
    $data = fread($database, $meta['record_length']);
    if (strlen($data) !== $meta['record_length']) {
      return false;
    }

    $is_deleted = ($data[0] === '*');
    $res = array();
    $pos = 1;
    foreach ($meta['fields'] as $f) {
      $raw_val = substr($data, $pos, $f['length']);
      if ($f['type'] === 'N' || $f['type'] === 'F') {
        $val = trim($raw_val);
        $res[$f['name']] = (strpos($val, '.') !== false ? (float)$val : (int)$val);
      } elseif ($f['type'] === 'L') {
        $val = strtoupper($raw_val[0] ?? ' ');
        if ($val === 'T' || $val === 'Y') {
          $res[$f['name']] = true;
        } elseif ($val === '?') {
          $res[$f['name']] = null;
        } else {
          $res[$f['name']] = false;
        }
      } elseif ($f['type'] === 'T') {
        $res[$f['name']] = _dbase_set_timestamp($raw_val);
      } else {
        $res[$f['name']] = $raw_val;
      }
      $pos += $f['length'];
    }

    $res['deleted'] = ($is_deleted ? 1 : 0);
    return $res;
  }
}

if (!function_exists('dbase_add_record')) {
  function dbase_add_record() {
    $num_args = func_num_args();
    if ($num_args != 2) {
      _dbase_arg_error('dbase_add_record', 2, $num_args);
      return null;
    }
    $args = func_get_args();
    $database = $args[0];
    $data = $args[1];

    $meta = DbasePolyfillState::get($database);
    if (!$meta) {
      if (PHP_VERSION_ID >= 80000 && !is_resource($database)) {
        _dbase_type_error('dbase_add_record', 1, 'database', 'resource', gettype($database));
        return null;
      }
      if (PHP_VERSION_ID < 80000 && !is_resource($database)) {
        trigger_error('dbase_add_record() expects parameter 1 to be resource, ' . gettype($database) . ' given', E_USER_WARNING);
      }
      if (PHP_VERSION_ID >= 80000) {
        throw new TypeError('dbase_add_record(): supplied resource is not a valid dbase resource');
      }
      trigger_error('dbase_add_record(): supplied resource is not a valid dbase resource', E_USER_WARNING);
      return false;
    }
    if (!is_array($data)) {
      _dbase_type_error('dbase_add_record', 2, 'data', 'array', gettype($data));
      return null;
    }
    if ($meta['mode'] === 0) {
      trigger_error('dbase_add_record(): unable to write to the file', E_USER_WARNING);
      return false;
    }
    $c_expected = count($meta['fields']);
    $c_got = count($data);
    if ($c_expected !== $c_got) {
      trigger_error("dbase_add_record(): expected $c_expected fields, but got $c_got", E_USER_WARNING);
      return false;
    }
    $i = 0;
    foreach ($data as $k => $v) {
      if ($k !== $i++) {
        trigger_error("dbase_add_record(): expected plain indexed array", E_USER_WARNING);
        return false;
      }
    }

    $record = ' ';
    $i = 0;
    $is_assoc = array_keys($data) !== range(0, count($data) - 1);

    foreach ($meta['fields'] as $f) {
      $val = $is_assoc ? (isset($data[$f['name']]) ? $data[$f['name']] : '') : (isset($data[$i]) ? $data[$i] : '');
      if ($f['type'] === 'C') {
        $record .= str_pad(substr((string)$val, 0, $f['length']), $f['length'], ' ', STR_PAD_RIGHT);
      } elseif ($f['type'] === 'N' || $f['type'] === 'F') {
        if (is_float($val)) {
          $str_val = number_format($val, $f['precision'], '.', '');
        } else {
          $str_val = (string)$val;
        }
        $len = $f['length'];
        if (strlen($str_val) < $len) {
          $record .= str_pad($str_val, $len, ' ', STR_PAD_LEFT);
        } else {
          $record .= substr($str_val, 0, $len);
        }
      } elseif ($f['type'] === 'T') {
        $record .= _dbase_get_timestamp((string)$val);
      } else {
        $str_val = substr((string)$val, 0, $f['length']);
        $record .= str_pad($str_val, $f['length'], ' ', STR_PAD_LEFT);
      }
      $i++;
    }

    fseek($database, $meta['header_length'] + $meta['records'] * $meta['record_length']);
    fwrite($database, $record);

    $meta['records']++;
    DbasePolyfillState::update($database, $meta);

    fwrite($database, "\x1A");
    fseek($database, 4);
    fwrite($database, pack('V', $meta['records']));
    _dbase_update_date($database);

    return true;
  }
}

if (!function_exists('dbase_replace_record')) {
  function dbase_replace_record() {
    $num_args = func_num_args();
    if ($num_args != 3) {
      _dbase_arg_error('dbase_replace_record', 3, $num_args);
      return null;
    }
    $args = func_get_args();
    $database = $args[0];
    $data = $args[1];
    $number = $args[2];

    $meta = DbasePolyfillState::get($database);
    if (!$meta) {
      if (PHP_VERSION_ID >= 80000 && !is_resource($database)) {
        _dbase_type_error('dbase_replace_record', 1, 'database', 'resource', gettype($database));
        return null;
      }
      if (PHP_VERSION_ID < 80000 && !is_resource($database)) {
        trigger_error('dbase_replace_record() expects parameter 1 to be resource, ' . gettype($database) . ' given', E_USER_WARNING);
      }
      if (PHP_VERSION_ID >= 80000) {
        throw new TypeError('dbase_replace_record(): supplied resource is not a valid dbase resource');
      }
      trigger_error('dbase_replace_record(): supplied resource is not a valid dbase resource', E_USER_WARNING);
      return false;
    }
    if (!is_array($data)) {
      _dbase_type_error('dbase_replace_record', 2, 'data', 'array', gettype($data));
      return null;
    }
    if (!is_int($number)) {
      _dbase_type_error('dbase_replace_record', 3, 'number', 'int', gettype($number));
      return null;
    }
    if ($meta['mode'] === 0) {
      trigger_error('dbase_replace_record(): unable to write to the file', E_USER_WARNING);
      return false;
    }
    if ($number < 1 || $number > $meta['records']) {
      return false;
    }

    $c_expected = count($meta['fields']);
    $c_got = count($data);
    if ($c_expected !== $c_got) {
      trigger_error("dbase_replace_record(): expected $c_expected fields, but got $c_got", E_USER_WARNING);
      return false;
    }
    $i = 0;
    foreach ($data as $k => $v) {
      if ($k !== $i++) {
        trigger_error("dbase_replace_record(): expected plain indexed array", E_USER_WARNING);
        return false;
      }
    }

    $record = ' ';
    $i = 0;
    $is_assoc = (array_keys($data) !== range(0, count($data) - 1));

    foreach ($meta['fields'] as $f) {
      $val = $is_assoc ? (isset($data[$f['name']]) ? $data[$f['name']] : '') : (isset($data[$i]) ? $data[$i] : '');
      if ($f['type'] === 'C') {
        $record .= str_pad(substr((string)$val, 0, $f['length']), $f['length'], ' ', STR_PAD_RIGHT);
      } elseif ($f['type'] === 'N' || $f['type'] === 'F') {
        if (is_float($val)) {
          $str_val = number_format($val, $f['precision'], '.', '');
        } else {
          $str_val = (string)$val;
        }
        $len = $f['length'];
        if (strlen($str_val) < $len) {
          $record .= str_pad($str_val, $len, ' ', STR_PAD_LEFT);
        } else {
          $record .= substr($str_val, 0, $len);
        }
      } elseif ($f['type'] === 'T') {
        $record .= _dbase_get_timestamp((string)$val);
      } else {
        $str_val = substr((string)$val, 0, $f['length']);
        $record .= str_pad($str_val, $f['length'], ' ', STR_PAD_LEFT);
      }
      $i++;
    }

    $offset = $meta['header_length'] + ($number - 1) * $meta['record_length'];
    fseek($database, $offset);
    $current_deleted = fread($database, 1);
    fseek($database, $offset);
    $record[0] = $current_deleted;
    fwrite($database, $record);
    _dbase_update_date($database);
    return true;
  }
}

if (!function_exists('dbase_delete_record')) {
  function dbase_delete_record() {
    $num_args = func_num_args();
    if ($num_args != 2) {
      _dbase_arg_error('dbase_delete_record', 2, $num_args);
      return null;
    }
    $args = func_get_args();
    $database = $args[0];
    $number = $args[1];

    $meta = DbasePolyfillState::get($database);
    if (!$meta) {
      if (PHP_VERSION_ID >= 80000 && !is_resource($database)) {
        _dbase_type_error('dbase_delete_record', 1, 'database', 'resource', gettype($database));
        return null;
      }
      if (PHP_VERSION_ID < 80000 && !is_resource($database)) {
        trigger_error('dbase_delete_record() expects parameter 1 to be resource, ' . gettype($database) . ' given', E_USER_WARNING);
      }
      if (PHP_VERSION_ID >= 80000) {
        throw new TypeError('dbase_delete_record(): supplied resource is not a valid dbase resource');
      }
      trigger_error('dbase_delete_record(): supplied resource is not a valid dbase resource', E_USER_WARNING);
      return false;
    }
    if (!is_int($number)) {
      _dbase_type_error('dbase_delete_record', 2, 'number', 'int', gettype($number));
      return null;
    }
    if ($meta['mode'] === 0) {
      trigger_error('dbase_delete_record(): unable to write to the file', E_USER_WARNING);
      return false;
    }
    if ($number < 1 || $number > $meta['records']) {
      return false;
    }

    fseek($database, ($meta['header_length'] + ($number - 1) * $meta['record_length']));
    fwrite($database, '*');
    _dbase_update_date($database);
    return true;
  }
}

if (!function_exists('dbase_pack')) {
  function dbase_pack() {
    $num_args = func_num_args();
    if ($num_args != 1) {
      _dbase_arg_error('dbase_pack', 1, $num_args);
      return null;
    }
    $database = func_get_arg(0);

    $meta = DbasePolyfillState::get($database);
    if (!$meta) {
      if (PHP_VERSION_ID >= 80000 && !is_resource($database)) {
        _dbase_type_error('dbase_pack', 1, 'database', 'resource', gettype($database));
        return null;
      }
      if (PHP_VERSION_ID < 80000 && !is_resource($database)) {
        trigger_error('dbase_pack() expects parameter 1 to be resource, ' . gettype($database) . ' given', E_USER_WARNING);
      }
      if (PHP_VERSION_ID >= 80000) {
        throw new TypeError('dbase_pack(): supplied resource is not a valid dbase resource');
      }
      trigger_error('dbase_pack(): supplied resource is not a valid dbase resource', E_USER_WARNING);
      return false;
    }
    if ($meta['mode'] === 0) {
      trigger_error('dbase_pack(): unable to write to the file', E_USER_WARNING);
      return false;
    }

    $temp_file = tempnam(sys_get_temp_dir(), 'dbf');
    $fp = fopen($temp_file, 'w+b');

    fseek($database, 0);
    $header = fread($database, $meta['header_length']);
    fwrite($fp, $header);

    $new_records = 0;
    for ($i = 1; $i <= $meta['records']; $i++) {
      fseek($database, $meta['header_length'] + ($i - 1) * $meta['record_length']);
      $record = fread($database, $meta['record_length']);
      if ($record[0] !== '*') {
        fwrite($fp, $record);
        $new_records++;
      }
    }
    fwrite($fp, "\x1A");

    fflush($fp);
    fseek($fp, 0);
    ftruncate($database, 0);
    fseek($database, 0);
    stream_copy_to_stream($fp, $database);
    fclose($fp);
    unlink($temp_file);

    fseek($database, 4);
    fwrite($database, pack('V', $new_records));
    _dbase_update_date($database);

    $meta['records'] = $new_records;
    DbasePolyfillState::update($database, $meta);

    return true;
  }
}
