/*
 * Copyright (C) 2013 FUJITSU LIMITED.  All rights reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public
 * License v2 as published by the Free Software Foundation.
 *
 * 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, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 021110-1307, USA.
 */

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <uuid/uuid.h>
#include <errno.h>
#include <unistd.h>
#include <ctype.h>
#include <getopt.h>

#include "kerncompat.h"
#include "ctree.h"
#include "disk-io.h"
#include "list.h"
#include "utils.h"
#include "crc32c.h"
#include "volumes.h"
#include "commands.h"

struct btrfs_recover_superblock {
	struct btrfs_fs_devices *fs_devices;

	struct list_head good_supers;
	struct list_head bad_supers;

	u64 max_generation;
};

struct super_block_record {
	struct list_head list;

	char *device_name;
	struct btrfs_super_block sb;

	u64 bytenr;
};

static
void init_recover_superblock(struct btrfs_recover_superblock *recover)
{
	INIT_LIST_HEAD(&recover->good_supers);
	INIT_LIST_HEAD(&recover->bad_supers);

	recover->fs_devices = NULL;
	recover->max_generation = 0;
}

static
void free_recover_superblock(struct btrfs_recover_superblock *recover)
{
	struct super_block_record *record;

	if (!recover->fs_devices)
		return;

	while (!list_empty(&recover->good_supers)) {
		record = list_entry(recover->good_supers.next,
				struct super_block_record, list);
		list_del_init(&record->list);
		free(record->device_name);
		free(record);
	}

	while (!list_empty(&recover->bad_supers)) {
		record = list_entry(recover->bad_supers.next,
				struct super_block_record, list);
		list_del_init(&record->list);
		free(record->device_name);
		free(record);
	}
}

static int add_superblock_record(struct btrfs_super_block *sb, char *fname,
			u64 bytenr, struct list_head *head)
{
	struct super_block_record *record;

	record = malloc(sizeof(struct super_block_record));
	if (!record)
		return -ENOMEM;

	record->device_name = strdup(fname);
	if (!record->device_name) {
		free(record);
		return -ENOMEM;
	}
	memcpy(&record->sb, sb, sizeof(*sb));
	record->bytenr = bytenr;
	list_add_tail(&record->list, head);

	return 0;
}

static int
read_dev_supers(char *filename, struct btrfs_recover_superblock *recover)
{
	int i, ret, fd;
	u8 buf[BTRFS_SUPER_INFO_SIZE];
	u64 max_gen, bytenr;
	struct btrfs_super_block *sb = (struct btrfs_super_block *)buf;

	/* just ignore errno that were set in btrfs_scan_fs_devices() */
	errno = 0;

	fd = open(filename, O_RDONLY);
	if (fd < 0)
		return -errno;

	for (i = 0; i < BTRFS_SUPER_MIRROR_MAX; i++) {
		bytenr = btrfs_sb_offset(i);

		ret = btrfs_read_dev_super(fd, sb, bytenr, SBREAD_DEFAULT);
		if (!ret) {
			ret = add_superblock_record(sb, filename, bytenr,
							&recover->good_supers);
			if (ret)
				goto out;
			max_gen = btrfs_super_generation(sb);
			if (max_gen > recover->max_generation)
				recover->max_generation = max_gen;
		} else if (ret == -EIO){
			/*
			 * Skip superblock which doesn't exist, only adds
			 * really corrupted superblock
			 */
			ret = add_superblock_record(sb, filename, bytenr,
						&recover->bad_supers);
			if (ret)
				goto out;
		}
	}
	ret = 0;
out:
	close(fd);
	return ret;
}

static int read_fs_supers(struct btrfs_recover_superblock *recover)
{
	struct super_block_record *record;
	struct super_block_record *next_record;
	struct btrfs_device *dev;
	int ret;
	u64 gen;

	list_for_each_entry(dev, &recover->fs_devices->devices,
				dev_list) {
		ret = read_dev_supers(dev->name, recover);
		if (ret)
			return ret;
	}
	list_for_each_entry_safe(record, next_record,
			&recover->good_supers, list) {
		gen = btrfs_super_generation(&record->sb);
		if (gen < recover->max_generation)
			list_move_tail(&record->list, &recover->bad_supers);
	}

	return 0;
}

static struct super_block_record *recover_get_good_super(
				struct btrfs_recover_superblock *recover)
{
	struct super_block_record *record;
	record = list_entry(recover->good_supers.next,
				struct super_block_record, list);
	return record;
}

static void print_all_devices(struct list_head *devices)
{
	struct btrfs_device *dev;

	printf("All Devices:\n");
	list_for_each_entry(dev, devices, dev_list) {
		printf("\t");
		printf("Device: id = %llu, name = %s\n",
			dev->devid, dev->name);
	}
	printf("\n");
}

static void print_super_info(struct super_block_record *record)
{
	printf("\t\tdevice name = %s\n", record->device_name);
	printf("\t\tsuperblock bytenr = %llu\n", record->bytenr);
}

static void print_all_supers(struct btrfs_recover_superblock *recover)
{
	struct super_block_record *record;

	printf("\t[All good supers]:\n");
	list_for_each_entry(record, &recover->good_supers, list) {
		print_super_info(record);
		printf("\n");
	}

	printf("\t[All bad supers]:\n");
	list_for_each_entry(record, &recover->bad_supers, list) {
		print_super_info(record);
		printf("\n");
	}
	printf("\n");
}

static void recover_err_str(int ret)
{
	switch (ret) {
	case 0:
		printf("All supers are valid, no need to recover\n");
		break;
	case 1:
		printf("Usage or syntax errors\n");
		break;
	case 2:
		printf("Recovered bad superblocks successful\n");
		break;
	case 3:
		printf("Failed to recover bad superblocks\n");
		break;
	case 4:
		printf("Aborted to recover bad superblocks\n");
		break;
	default:
		printf("Unknown recover result\n");
		break;
	}
}

int btrfs_recover_superblocks(const char *dname,
			int verbose, int yes)
{
	int fd, ret;
	struct btrfs_recover_superblock recover;
	struct super_block_record *record;
	struct btrfs_root *root = NULL;

	fd = open(dname, O_RDONLY);
	if (fd < 0) {
		fprintf(stderr, "open %s error\n", dname);
		return 1;
	}
	init_recover_superblock(&recover);

	ret = btrfs_scan_fs_devices(fd, dname, &recover.fs_devices, 0,
			SBREAD_RECOVER, 0);
	close(fd);
	if (ret) {
		ret = 1;
		goto no_recover;
	}

	if (verbose)
		print_all_devices(&recover.fs_devices->devices);

	ret = read_fs_supers(&recover);
	if (ret) {
		ret = 1;
		goto no_recover;
	}
	if (verbose) {
		printf("Before Recovering:\n");
		print_all_supers(&recover);
	}

	if (list_empty(&recover.bad_supers))
		goto no_recover;

	if (!yes) {
		ret = ask_user("Make sure this is a btrfs disk otherwise the tool will destroy other fs, Are you sure?");
		if (!ret) {
			ret = 4;
			goto no_recover;
		}
	}
	record = recover_get_good_super(&recover);
	root = open_ctree(record->device_name, record->bytenr,
			  OPEN_CTREE_RECOVER_SUPER | OPEN_CTREE_WRITES);
	if (!root) {
		ret = 3;
		goto no_recover;
	}
	/* reset super_bytenr in order that we will rewrite all supers */
	root->fs_info->super_bytenr = BTRFS_SUPER_INFO_OFFSET;
	ret = write_all_supers(root->fs_info);
	if (!ret)
		ret = 2;
	else
		ret = 3;

	close_ctree(root);
no_recover:
	recover_err_str(ret);
	free_recover_superblock(&recover);
	/* check if we have freed fs_devices in close_ctree() */
	if (!root)
		btrfs_close_devices(recover.fs_devices);
	return ret;
}

