/*
 * Copyright (c) 2020 Tomohiro Kusumi <tkusumi@netbsd.org>
 * Copyright (c) 2020 The DragonFly Project
 * All rights reserved.
 *
 * This code is derived from software contributed to The DragonFly Project
 * by Matthew Dillon <dillon@dragonflybsd.org>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 * 3. Neither the name of The DragonFly Project nor the names of its
 *    contributors may be used to endorse or promote products derived
 *    from this software without specific, prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE
 * COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <fstab.h>
#include <assert.h>
#include <errno.h>
#include <err.h>

#include <vfs/hammer2/hammer2_disk.h>

#include "hammer2_subs.h"

static hammer2_ondisk_t fso;
static int hammer2_volumes_initialized;

static void
hammer2_init_volume(hammer2_volume_t *vol)
{
	vol->fd = -1;
	vol->id = -1;
	vol->offset = (hammer2_off_t)-1;
	vol->size = (hammer2_off_t)-1;
}

void
hammer2_init_ondisk(hammer2_ondisk_t *fsp)
{
	int i;

	bzero(fsp, sizeof(*fsp));
	fsp->version = -1;
	for (i = 0; i < HAMMER2_MAX_VOLUMES; ++i)
		hammer2_init_volume(&fsp->volumes[i]);
}

void
hammer2_install_volume(hammer2_volume_t *vol, int fd, int id, const char *path,
		       hammer2_off_t offset, hammer2_off_t size)
{
	bzero(vol, sizeof(*vol));
	vol->fd = fd;
	vol->id = id;
	vol->path = strdup(path);
	vol->offset = offset;
	vol->size = size;
}

void
hammer2_uninstall_volume(hammer2_volume_t *vol)
{
	fsync(vol->fd);
	close(vol->fd);
	free(vol->path);
	hammer2_init_volume(vol);
}

/*
 * Locate a valid volume header.  If any of the four volume headers is good,
 * we have a valid volume header and choose the best one based on mirror_tid.
 */
static int
hammer2_read_volume_header(int fd, const char *path,
			   hammer2_volume_data_t *voldata)
{
	hammer2_volume_data_t vd;
	hammer2_tid_t mirror_tid = -1;
	hammer2_off_t size = check_volume(fd);
	hammer2_crc32_t crc0, crc1;
	const char *p;
	int i, zone = -1;
	ssize_t ret;

	for (i = 0; i < HAMMER2_NUM_VOLHDRS; ++i) {
		if (i * HAMMER2_ZONE_BYTES64 >= size)
			break;
		if (lseek(fd, i * HAMMER2_ZONE_BYTES64, SEEK_SET) == -1)
			break;
		ret = read(fd, &vd, HAMMER2_VOLUME_BYTES);
		if (ret == -1) {
			fprintf(stderr, "%s #%d: read %s\n",
				path, i, strerror(errno));
			continue;
		}
		if (ret != HAMMER2_VOLUME_BYTES) {
			fprintf(stderr, "%s #%d: read %s\n",
				path, i, strerror(errno));
			continue;
		}

		p = (const char*)&vd;
		/* verify volume header magic */
		if ((vd.magic != HAMMER2_VOLUME_ID_HBO) &&
		    (vd.magic != HAMMER2_VOLUME_ID_ABO)) {
			fprintf(stderr, "%s #%d: bad magic\n", path, i);
			continue;
		}

		if (vd.magic == HAMMER2_VOLUME_ID_ABO) {
			/* XXX: Reversed-endianness filesystem */
			fprintf(stderr,
				"%s #%d: reverse-endian filesystem detected",
				path, i);
			continue;
		}

		/* verify volume header CRC's */
		crc0 = vd.icrc_sects[HAMMER2_VOL_ICRC_SECT0];
		crc1 = hammer2_icrc32(p + HAMMER2_VOLUME_ICRC0_OFF,
				      HAMMER2_VOLUME_ICRC0_SIZE);
		if (crc0 != crc1) {
			fprintf(stderr,
				"%s #%d: volume header crc mismatch "
				"sect0 %08x/%08x\n",
				path, i, crc0, crc1);
			continue;
		}

		crc0 = vd.icrc_sects[HAMMER2_VOL_ICRC_SECT1];
		crc1 = hammer2_icrc32(p + HAMMER2_VOLUME_ICRC1_OFF,
				      HAMMER2_VOLUME_ICRC1_SIZE);
		if (crc0 != crc1) {
			fprintf(stderr,
				"%s #%d: volume header crc mismatch "
				"sect1 %08x/%08x",
				path, i, crc0, crc1);
			continue;
		}

		crc0 = vd.icrc_volheader;
		crc1 = hammer2_icrc32(p + HAMMER2_VOLUME_ICRCVH_OFF,
				      HAMMER2_VOLUME_ICRCVH_SIZE);
		if (crc0 != crc1) {
			fprintf(stderr,
				"%s #%d: volume header crc mismatch "
				"vh %08x/%08x",
				path, i, crc0, crc1);
			continue;
		}
		if (zone == -1 || mirror_tid < vd.mirror_tid) {
			bcopy(&vd, voldata, sizeof(vd));
			mirror_tid = vd.mirror_tid;
			zone = i;
		}
	}
	return(zone);
}

static void
hammer2_err_uuid_mismatch(uuid_t *uuid1, uuid_t *uuid2, const char *id)
{
	char *p1 = NULL, *p2 = NULL;

	hammer2_uuid_to_str(uuid1, &p1);
	hammer2_uuid_to_str(uuid2, &p2);

	errx(1, "Volume %s uuid mismatch %s vs %s", id, p1, p2);

	free(p1);
	free(p2);
}

static void
hammer2_add_volume(const char *path, int rdonly)
{
	hammer2_volume_data_t voldata;
	hammer2_volume_t *vol;
	struct stat st;
	int fd, i;
	uuid_t uuid;

	fd = open(path, rdonly ? O_RDONLY : O_RDWR);
	if (fd == -1)
		err(1, "open");

	if (fstat(fd, &st) == -1)
		err(1, "fstat");
	if (!S_ISCHR(st.st_mode) && !S_ISREG(st.st_mode))
		errx(1, "Unsupported file type");

	if (hammer2_read_volume_header(fd, path, &voldata) >= 0) {
		i = voldata.volu_id;
		if (i < 0 || i >= HAMMER2_MAX_VOLUMES)
			errx(1, "%s has bad volume id %d", path, i);
		vol = &fso.volumes[i];
		if (vol->id != -1)
			errx(1, "volume id %d already initialized", i);
		/* all headers must have the same version, nvolumes and uuid */
		if (!fso.nvolumes) {
			fso.version = voldata.version;
			fso.nvolumes = voldata.nvolumes;
			fso.fsid = voldata.fsid;
			fso.fstype = voldata.fstype;
		} else {
			if (fso.version != (int)voldata.version)
				errx(1, "Volume version mismatch %d vs %d",
				     fso.version, (int)voldata.version);
			if (fso.nvolumes != voldata.nvolumes)
				errx(1, "Volume count mismatch %d vs %d",
				     fso.nvolumes, voldata.nvolumes);
			uuid = voldata.fsid;
			if (!uuid_equal(&fso.fsid, &uuid, NULL))
				hammer2_err_uuid_mismatch(&fso.fsid,
							  &uuid,
							  "fsid");
			uuid = voldata.fstype;
			if (!uuid_equal(&fso.fstype, &uuid, NULL))
				hammer2_err_uuid_mismatch(&fso.fstype,
							  &uuid,
							  "fstype");
		}
		/* all per-volume tests passed */
		hammer2_install_volume(vol, fd, i, path,
				       voldata.volu_loff[i], voldata.volu_size);
		fso.total_size += vol->size;
	} else {
		errx(1, "No valid volume headers found!");
	}
}

static void
hammer2_verify_volumes_common(const hammer2_ondisk_t *fsp,
			      const hammer2_volume_data_t *rootvoldata)
{
	const hammer2_volume_t *vol;
	hammer2_off_t size;
	struct stat *st;
	const char *path;
	char *str;
	int i, j, nvolumes = 0;
	uuid_t uuid;

	if (fsp->version == -1)
		errx(1, "Bad volume version %d", fsp->version);

	/* check initialized volume count */
	for (i = 0; i < HAMMER2_MAX_VOLUMES; ++i) {
		vol = &fsp->volumes[i];
		if (vol->id != -1)
			nvolumes++;
	}

	/* check volume header */
	if (rootvoldata) {
		if (rootvoldata->volu_id != HAMMER2_ROOT_VOLUME)
			errx(1, "Volume id %d must be %d",
			     rootvoldata->volu_id, HAMMER2_ROOT_VOLUME);
		str = NULL;
		uuid = rootvoldata->fstype;
		hammer2_uuid_to_str(&uuid, &str);
		if (strcmp(str, HAMMER2_UUID_STRING))
			errx(1, "Volume fstype uuid %s must be %s", str,
			     HAMMER2_UUID_STRING);
	}

	/* fsp->nvolumes hasn't been verified yet, use nvolumes */
	st = calloc(nvolumes, sizeof(*st));

	for (i = 0; i < HAMMER2_MAX_VOLUMES; ++i) {
		vol = &fsp->volumes[i];
		if (vol->id == -1)
			continue;
		path = vol->path;
		/* check volumes are unique */
		if (stat(path, &st[i]) != 0)
			errx(1, "Failed to stat %s", path);
		if (fstat(vol->fd, &st[i]) != 0)
			errx(1, "Failed to fstat %d", vol->fd);
		for (j = 0; j < i; ++j) {
			if ((st[i].st_ino == st[j].st_ino) &&
			    (st[i].st_dev == st[j].st_dev))
				errx(1, "%s specified more than once", path);
		}
		/* check volume fields are initialized */
		if (vol->fd == -1)
			errx(1, "%s has bad fd %d", path, vol->fd);
		if (vol->offset == (hammer2_off_t)-1)
			errx(1, "%s has bad offset 0x%016jx", path,
			     (intmax_t)vol->offset);
		if (vol->size == (hammer2_off_t)-1)
			errx(1, "%s has bad size 0x%016jx", path,
			     (intmax_t)vol->size);
		/* check volume size vs block device size */
		size = check_volume(vol->fd);
		printf("checkvolu header %d %016jx/%016jx\n", i, vol->size, size);
		if (vol->size > size)
			errx(1, "%s's size 0x%016jx exceeds device size 0x%016jx",
			     path, (intmax_t)vol->size, size);
		if (vol->size == 0)
			errx(1, "%s has size of 0", path);
	}
	free(st);
}

static void
hammer2_verify_volumes_1(hammer2_ondisk_t *fsp,
			 const hammer2_volume_data_t *rootvoldata)
{
	const hammer2_volume_t *vol;
	hammer2_off_t off;
	const char *path;
	int i, nvolumes = 0;

	/* check initialized volume count */
	for (i = 0; i < HAMMER2_MAX_VOLUMES; ++i) {
		vol = &fsp->volumes[i];
		if (vol->id != -1)
			nvolumes++;
	}
	if (nvolumes != 1)
		errx(1, "Only 1 volume supported");
	fsp->nvolumes = nvolumes; /* adjust with actual count */

	/* check volume header */
	if (rootvoldata) {
		if (rootvoldata->nvolumes)
			errx(1, "Volume count %d must be 0",
			     rootvoldata->nvolumes);
		if (rootvoldata->total_size)
			errx(1, "Total size 0x%016jx must be 0",
			     (intmax_t)rootvoldata->total_size);
		for (i = 0; i < HAMMER2_MAX_VOLUMES; ++i) {
			off = rootvoldata->volu_loff[i];
			if (off)
				errx(1, "Volume offset[%d] 0x%016jx must be 0",
				     i, (intmax_t)off);
		}
	}

	/* check volume */
	vol = &fsp->volumes[HAMMER2_ROOT_VOLUME];
	path = vol->path;
	if (vol->id)
		errx(1, "%s has non zero id %d", path, vol->id);
	if (vol->offset)
		errx(1, "%s has non zero offset 0x%016jx", path,
		     (intmax_t)vol->offset);
	if (vol->size & HAMMER2_VOLUME_ALIGNMASK64)
		errx(1, "%s's size is not 0x%016jx aligned", path,
		     (intmax_t)HAMMER2_VOLUME_ALIGN);
}

static void
hammer2_verify_volumes_2(const hammer2_ondisk_t *fsp,
			 const hammer2_volume_data_t *rootvoldata)
{
	const hammer2_volume_t *vol;
	hammer2_off_t off;
	const char *path;
	int i, nvolumes = 0;

	/* check initialized volume count */
	for (i = 0; i < HAMMER2_MAX_VOLUMES; ++i) {
		vol = &fsp->volumes[i];
		if (vol->id != -1)
			nvolumes++;
	}
	if (fsp->nvolumes != nvolumes)
		errx(1, "Volume count mismatch %d vs %d",
		     fsp->nvolumes, nvolumes);

	/* check volume header */
	if (rootvoldata) {
		if (rootvoldata->nvolumes != fso.nvolumes)
			errx(1, "Volume header requires %d devices, %d specified",
			     rootvoldata->nvolumes, fso.nvolumes);
		if (rootvoldata->total_size != fso.total_size)
			errx(1, "Total size 0x%016jx does not equal sum of "
			     "volumes 0x%016jx",
			     rootvoldata->total_size, fso.total_size);
		for (i = 0; i < nvolumes; ++i) {
			off = rootvoldata->volu_loff[i];
			if (off == (hammer2_off_t)-1)
				errx(1, "Volume offset[%d] 0x%016jx must not be -1",
				     i, (intmax_t)off);
		}
		for (i = nvolumes; i < HAMMER2_MAX_VOLUMES; ++i) {
			off = rootvoldata->volu_loff[i];
			if (off != (hammer2_off_t)-1)
				errx(1, "Volume offset[%d] 0x%016jx must be -1",
				     i, (intmax_t)off);
		}
	}

	/* check volumes */
	for (i = 0; i < HAMMER2_MAX_VOLUMES; ++i) {
		vol = &fsp->volumes[i];
		if (vol->id == -1)
			continue;
		path = vol->path;
		/* check offset */
		if (vol->offset & HAMMER2_FREEMAP_LEVEL1_MASK)
			errx(1, "%s's offset 0x%016jx not 0x%016jx aligned",
			     path, (intmax_t)vol->offset,
			     HAMMER2_FREEMAP_LEVEL1_SIZE);
		/* check vs previous volume */
		if (i) {
			if (vol->id != (vol-1)->id + 1)
				errx(1, "%s has inconsistent id %d", path,
				     vol->id);
			if (vol->offset != (vol-1)->offset + (vol-1)->size)
				errx(1, "%s has inconsistent offset 0x%016jx",
				     path, (intmax_t)vol->offset);
		} else { /* first */
			if (vol->offset)
				errx(1, "%s has non zero offset 0x%016jx", path,
				     (intmax_t)vol->offset);
		}
		/* check size for non-last and last volumes */
		if (i != fsp->nvolumes - 1) {
			if (vol->size < HAMMER2_FREEMAP_LEVEL1_SIZE)
				errx(1, "%s's size must be >= 0x%016jx", path,
				     (intmax_t)HAMMER2_FREEMAP_LEVEL1_SIZE);
			if (vol->size & HAMMER2_FREEMAP_LEVEL1_MASK)
				errx(1, "%s's size is not 0x%016jx aligned",
				     path,
				     (intmax_t)HAMMER2_FREEMAP_LEVEL1_SIZE);
		} else { /* last */
			if (vol->size & HAMMER2_VOLUME_ALIGNMASK64)
				errx(1, "%s's size is not 0x%016jx aligned",
				     path, (intmax_t)HAMMER2_VOLUME_ALIGN);
		}
	}
}

void
hammer2_verify_volumes(hammer2_ondisk_t *fsp,
		       const hammer2_volume_data_t *rootvoldata)
{
	hammer2_verify_volumes_common(fsp, rootvoldata);
	if (fsp->version >= HAMMER2_VOL_VERSION_MULTI_VOLUMES)
		hammer2_verify_volumes_2(fsp, rootvoldata);
	else
		hammer2_verify_volumes_1(fsp, rootvoldata);
	assert(fsp->nvolumes > 0);
}

void
hammer2_print_volumes(const hammer2_ondisk_t *fsp)
{
	const hammer2_volume_t *vol;
	int i, n, w = 0;

	for (i = 0; i < fsp->nvolumes; ++i) {
		vol = &fsp->volumes[i];
		n = (int)strlen(vol->path);
		if (n > w)
			w = n;
	}

	printf("total    %-*.*s 0x%016jx 0x%016jx\n",
		w, w, "", (intmax_t)0, (intmax_t)fsp->total_size);

	for (i = 0; i < fsp->nvolumes; ++i) {
		vol = &fsp->volumes[i];
		printf("volume%-2d %-*.*s 0x%016jx 0x%016jx%s\n",
		       vol->id, w, w, vol->path, (intmax_t)vol->offset,
		       (intmax_t)vol->size,
		       (vol->id == HAMMER2_ROOT_VOLUME ?
		       " (root volume)" : ""));
	}
}

void
hammer2_init_volumes(const char *blkdevs, int rdonly)
{
	hammer2_volume_data_t *rootvoldata;
	char *o, *p, *devpath;

	if (hammer2_volumes_initialized)
		errx(1, "Already initialized");
	if (!blkdevs)
		errx(1, "NULL blkdevs");

	hammer2_init_ondisk(&fso);
	o = p = strdup(blkdevs);
	while ((devpath = p) != NULL) {
		if ((p = strchr(p, ':')) != NULL)
			*p++ = 0;
		devpath = getdevpath(devpath, 0);
		if (strchr(devpath, ':'))
			hammer2_init_volumes(devpath, rdonly);
		else
			hammer2_add_volume(devpath, rdonly);
		free(devpath);
	}
	free(o);
	hammer2_volumes_initialized = 1;

	rootvoldata = hammer2_read_root_volume_header();
	hammer2_verify_volumes(&fso, rootvoldata);
	free(rootvoldata);
}

void
hammer2_cleanup_volumes(void)
{
	hammer2_volume_t *vol;
	int i;

	for (i = 0; i < HAMMER2_MAX_VOLUMES; ++i) {
		vol = &fso.volumes[i];
		if (vol->id == -1)
			continue;
		hammer2_uninstall_volume(vol);
	}
	hammer2_volumes_initialized = 0;
}

typedef void (*callback)(const hammer2_volume_t*, void *data);

hammer2_volume_t *
hammer2_get_volume(hammer2_off_t offset)
{
	hammer2_volume_t *vol;
	int i;

	assert(hammer2_volumes_initialized == 1);
	offset &= ~HAMMER2_OFF_MASK_RADIX;

	/* do binary search if users really use this many supported volumes */
	for (i = 0; i < fso.nvolumes; ++i) {
		vol = &fso.volumes[i];
		if ((offset >= vol->offset) &&
		    (offset < vol->offset + vol->size))
		{
			return vol;
		}
	}
	return NULL;
}

static int
hammer2_get_volume_attr(hammer2_off_t offset, callback fn, void *data)
{
	hammer2_volume_t *vol;
	int i;

	assert(hammer2_volumes_initialized == 1);
	offset &= ~HAMMER2_OFF_MASK_RADIX;

	/* do binary search if users really use this many supported volumes */
	for (i = 0; i < fso.nvolumes; ++i) {
		vol = &fso.volumes[i];
		if ((offset >= vol->offset) &&
		    (offset < vol->offset + vol->size)) {
			fn(vol, data);
			return(0);
		}
	}

	return(-1);
}

/* fd */
static void
hammer2_volume_fd_cb(const hammer2_volume_t *vol, void *data)
{
	*(int*)data = vol->fd;
}

int
hammer2_get_volume_fd(hammer2_off_t offset)
{
	int ret = 0;

	if (hammer2_get_volume_attr(offset, hammer2_volume_fd_cb, &ret) < 0)
		return(-1);
	return(ret);
}

int
hammer2_get_root_volume_fd(void)
{
	return(hammer2_get_volume_fd(0));
}

/* id */
static void
hammer2_volume_id_cb(const hammer2_volume_t *vol, void *data)
{
	*(int*)data = vol->id;
}

int
hammer2_get_volume_id(hammer2_off_t offset)
{
	int ret = 0;

	if (hammer2_get_volume_attr(offset, hammer2_volume_id_cb, &ret) < 0)
		return(-1);
	return(ret);
}

int
hammer2_get_root_volume_id(void)
{
	return(hammer2_get_volume_id(0));
}

/* path */
static void
hammer2_volume_path_cb(const hammer2_volume_t *vol, void *data)
{
	*(const char**)data = vol->path;
}

const char *
hammer2_get_volume_path(hammer2_off_t offset)
{
	const char *ret = NULL;

	if (hammer2_get_volume_attr(offset, hammer2_volume_path_cb, &ret) < 0)
		return(NULL);
	return(ret);
}

const char *
hammer2_get_root_volume_path(void)
{
	return(hammer2_get_volume_path(0));
}

/* offset */
static void
hammer2_volume_offset_cb(const hammer2_volume_t *vol, void *data)
{
	*(hammer2_off_t*)data = vol->offset;
}

hammer2_off_t
hammer2_get_volume_offset(hammer2_off_t offset)
{
	hammer2_off_t ret = 0;

	if (hammer2_get_volume_attr(offset, hammer2_volume_offset_cb, &ret) < 0)
		return(-1);
	return(ret);
}

hammer2_off_t
hammer2_get_root_volume_offset(void)
{
	return(hammer2_get_volume_offset(0));
}

/* size */
static void
hammer2_volume_size_cb(const hammer2_volume_t *vol, void *data)
{
	*(hammer2_off_t*)data = vol->size;
}

hammer2_off_t
hammer2_get_volume_size(hammer2_off_t offset)
{
	hammer2_off_t ret = 0;

	if (hammer2_get_volume_attr(offset, hammer2_volume_size_cb, &ret) < 0)
		return(-1);
	return(ret);
}

hammer2_off_t
hammer2_get_root_volume_size(void)
{
	return(hammer2_get_volume_size(0));
}

/* total size */
hammer2_off_t
hammer2_get_total_size(void)
{
	return(fso.total_size);
}

hammer2_volume_data_t*
hammer2_read_root_volume_header(void)
{
	hammer2_volume_data_t *voldata;
	int fd = hammer2_get_root_volume_fd();
	const char *path = hammer2_get_root_volume_path();

	if (fd == -1)
		return(NULL);

	voldata = calloc(1, sizeof(*voldata));
	if (hammer2_read_volume_header(fd, path, voldata) >= 0)
		return(voldata);
	else
		errx(1, "Failed to read volume header");
}
