/* Copyright 2013-2017 IBM Corp.
 *
 * 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
 *
 * 	http://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.
 */

#include <skiboot.h>
#include <vpd.h>
#include <string.h>
#include "spira.h"
#include "hdata.h"
#include <device.h>
#include "hdata.h"
#include <inttypes.h>

struct card_info {
	const char *ccin; 	/* Customer card identification number */
	const char *description;
};

static const struct card_info card_table[] = {
	{"2B06", "System planar 2S4U"},
	{"2B07", "System planar 1S4U"},
	{"2B2E", "System planar 2S2U"},
	{"2B2F", "System planar 1S2U"},
	{"2CD4", "System planar 2S4U"},
	{"2CD5", "System planar 1S4U"},
	{"2CD6", "System planar 2S2U"},
	{"2CD7", "System planar 1S2U"},
	{"2CD7", "System planar 1S2U"},
	{"2B09", "Base JBOD, RAID and Backplane HD"},
	{"57D7", "Split JBOD, RAID Card"},
	{"2B0B", "Native I/O Card"},

	/* Anchor cards */
	{"52FE", "System Anchor Card - IBM Power 824"},
	{"52F2", "System Anchor Card - IBM Power 814"},
	{"52F5", "System Anchor Card - IBM Power 822"},
	{"561A", "System Anchor Card - IBM Power 824L"},
	{"524D", "System Anchor Card - IBM Power 822L"},
	{"560F", "System Anchor Card - IBM Power 812L"},
	{"561C", "System Anchor Card - DS8870"},

	/* Memory DIMMs */
	{"31E0", "16GB CDIMM"},
	{"31E8", "16GB CDIMM"},
	{"31E1", "32GB CDIMM"},
	{"31E9", "32GB CDIMM"},
	{"31E2", "64GB CDIMM"},
	{"31EA", "64GB CDIMM"},

	/* Power supplies */
	{"2B1D", "Power Supply 900W AC"},
	{"2B1E", "Power Supply 1400W AC"},
	{"2B75", "Power Supply 1400W HVDC"},

	/* Fans */
	{"2B1F", "Fan 4U (A1, A2, A3, A4)"},
	{"2B29", "Fan 2U (A1, A2, A3, A4, A5, A6)"},

	/* Other cards */
};

struct vpd_key_map {
	const char *keyword;		/* 2 char keyword  */
	const char *description;
};

static const struct vpd_key_map vpd_key_table[] = {
	{"AA", "ac-power-supply"},
	{"AM", "air-mover"},
	{"AV", "anchor-card"},

	{"BA", "bus-adapter-card"},
	{"BC", "battery-charger"},
	{"BD", "bus-daughter-card"},
	{"BE", "bus-expansion-card"},
	{"BP", "backplane"},
	{"BR", "backplane-riser"},
	{"BX", "backplane-extender"},

	{"CA", "calgary-bridge"},
	{"CB", "infiniband-connector"},
	{"CC", "clock-card"},
	{"CD", "card-connector"},
	{"CE", "ethernet-connector"},
	{"CL", "calgary-phb"},
	{"CI", "capacity-card"},
	{"CO", "sma-connector"},
	{"CP", "processor-capacity-card"},
	{"CR", "rio-connector"},
	{"CS", "serial-connector"},
	{"CU", "usb-connector"},

	{"DB", "dasd-backplane"},
	{"DC", "drawer-card"},
	{"DE", "drawer-extension"},
	{"DI", "drawer-interposer"},
	{"DL", "p7ih-dlink-connector"},
	{"DT", "legacy-pci-card"},
	{"DV", "media-drawer-led"},

	{"EI", "enclosure-led"},
	{"EF", "enclosure-fault-led"},
	{"ES", "embedded-sas"},
	{"ET", "ethernet-riser"},
	{"EV", "enclosure"},

	{"FM", "frame"},

	{"HB", "host-rio-pci-card"},
	{"HD", "high-speed-card"},
	{"HM", "hmc-connector"},

	{"IB", "io-backplane"},
	{"IC", "io-card"},
	{"ID", "ide-connector"},
	{"II", "io-drawer-led"},
	{"IP", "interplane-card"},
	{"IS", "smp-vbus-cable"},
	{"IT", "enclosure-cable"},
	{"IV", "io-enclosure"},

	{"KV", "keyboard-led"},

	{"L2", "l2-cache-module"},
	{"L3", "l3-cache-module"},
	{"LC", "squadrons-light-connector"},
	{"LR", "p7ih-connector"},
	{"LO", "system-locate-led"},
	{"LT", "squadrons-light-strip"},

	{"MB", "media-backplane"},
	{"ME", "map-extension"},
	{"MM", "mip-meter"},
	{"MS", "ms-dimm"},

	{"NB", "nvram-battery"},
	{"NC", "sp-node-controller"},
	{"ND", "numa-dimm"},

	{"OD", "cuod-card"},
	{"OP", "op-panel"},
	{"OS", "oscillator"},

	{"P2", "ioc"},
	{"P5", "ioc-bridge"},
	{"PB", "io-drawer-backplane"},
	{"PC", "power-capacitor"},
	{"PD", "processor-card"},
	{"PF", "processor"},
	{"PI", "ioc-phb"},
	{"PO", "spcn"},
	{"PN", "spcn-connector"},
	{"PR", "pci-riser-card"},
	{"PS", "power-supply"},
	{"PT", "pass-through-card"},
	{"PX", "psc-sync-card"},
	{"PW", "power-connector"},

	{"RG", "regulator"},
	{"RI", "riser"},
	{"RK", "rack-indicator"},
	{"RW", "riscwatch-connector"},

	{"SA", "sys-attn-led"},
	{"SB", "backup-sysvpd"},
	{"SC", "scsi-connector"},
	{"SD", "sas-connector"},
	{"SI", "scsi-ide-converter"},
	{"SL", "phb-slot"},
	{"SP", "service-processor"},
	{"SR", "service-card"},
	{"SS", "soft-switch"},
	{"SV", "system-vpd"},
	{"SY", "legacy-sysvpd"},

	{"TD", "tod-clock"},
	{"TI", "torrent-pcie-phb"},
	{"TL", "torrent-riser"},
	{"TM", "thermal-sensor"},
	{"TP", "tpmd-adapter"},
	{"TR", "torrent-bridge"},

	{"VV", "root-node-vpd"},

	{"WD", "water_device"},
};

static const char *vpd_map_name(const char *vpd_name)
{
	int i;

	for (i = 0; i < ARRAY_SIZE(vpd_key_table); i++)
		if (!strcmp(vpd_key_table[i].keyword, vpd_name))
			return vpd_key_table[i].description;

	prlog(PR_WARNING, "VPD: Could not map FRU ID %s to a known name\n",
	      vpd_name);

	return "Unknown";
}

static const struct card_info *card_info_lookup(char *ccin)
{
	int i;
	for(i = 0; i < ARRAY_SIZE(card_table); i++)
		/* CCIN is always 4 bytes in size */
		if (!strncmp(card_table[i].ccin, ccin, 4))
			return &card_table[i];
	return NULL;
}

/*
 * For OpenPOWER, we only decipher OPFR records. While OP HDAT have VINI
 * records too, populating the fields in there is optional. Also, there
 * is an overlap in the fields contained therein.
 */
static void vpd_opfr_parse(struct dt_node *node,
		const void *fruvpd, unsigned int fruvpd_sz)
{
	const void *kw;
	uint8_t sz;

	/* Vendor Name */
	kw = vpd_find(fruvpd, fruvpd_sz, "OPFR", "VN", &sz);
	if (kw)
		dt_add_property_nstr(node, "vendor", kw, sz);

	/* FRU Description */
	kw = vpd_find(fruvpd, fruvpd_sz, "OPFR", "DR", &sz);
	if (kw)
		dt_add_property_nstr(node, "description", kw, sz);

	/* Part number */
	kw = vpd_find(fruvpd, fruvpd_sz, "OPFR", "VP", &sz);
	if (kw)
		dt_add_property_nstr(node, "part-number", kw, sz);

	/* Serial number */
	kw = vpd_find(fruvpd, fruvpd_sz, "OPFR", "VS", &sz);
	if (kw)
		dt_add_property_nstr(node, "serial-number", kw, sz);

	/* Build date in BCD */
	kw = vpd_find(fruvpd, fruvpd_sz, "OPFR", "MB", &sz);
	if (kw)
		dt_add_property_nstr(node, "build-date", kw, sz);

	return;
}

static void vpd_vini_parse(struct dt_node *node,
			   const void *fruvpd, unsigned int fruvpd_sz)
{
	const void *kw;
	uint8_t sz;
	const struct card_info *cinfo;

	/* FRU Stocking Part Number */
	kw = vpd_find(fruvpd, fruvpd_sz, "VINI", "FN", &sz);
	if (kw)
		dt_add_property_nstr(node, "fru-number", kw, sz);

	/* Serial Number */
	kw = vpd_find(fruvpd, fruvpd_sz, "VINI", "SN", &sz);
	if (kw)
		dt_add_property_nstr(node, "serial-number", kw, sz);

	/* Part Number */
	kw = vpd_find(fruvpd, fruvpd_sz, "VINI", "PN", &sz);
	if (kw)
		dt_add_property_nstr(node, "part-number", kw, sz);

	/* CCIN Extension */
	kw = vpd_find(fruvpd, fruvpd_sz, "VINI", "CE", &sz);
	if (kw)
		dt_add_property_nstr(node, "ccin-extension", kw, sz);

	/* HW Version info */
	kw = vpd_find(fruvpd, fruvpd_sz, "VINI", "HW", &sz);
	if (kw)
		dt_add_property_nstr(node, "hw-version", kw, sz);

	/* Card type info */
	kw = vpd_find(fruvpd, fruvpd_sz, "VINI", "CT", &sz);
	if (kw)
		dt_add_property_nstr(node, "card-type", kw, sz);

	/* HW characteristics info */
	kw = vpd_find(fruvpd, fruvpd_sz, "VINI", "B3", &sz);
	if (kw)
		dt_add_property_nstr(node, "hw-characteristics", kw, sz);

	/* Customer Card Identification Number (CCIN) */
	kw = vpd_find(fruvpd, fruvpd_sz, "VINI", "CC", &sz);
	if (kw) {
		dt_add_property_nstr(node, "ccin", kw, sz);

		cinfo = card_info_lookup((char *)kw);
		if (cinfo) {
			dt_add_property_string(node,
				       "description", cinfo->description);
		} else {
			kw = vpd_find(fruvpd, fruvpd_sz, "VINI", "DR", &sz);
			if (kw) {
				dt_add_property_nstr(node,
						     "description", kw, sz);
			} else {
				dt_add_property_string(node, "description", "Unknown");
				prlog(PR_WARNING,
				      "VPD: CCIN desc not available for: %s\n",
				      (char *)kw);
			}
		}
	}

	return;
}

static bool valid_child_entry(const struct slca_entry *entry)
{
	if ((entry->install_indic == SLCA_INSTALL_INSTALLED) &&
		(entry->vpd_collected == SLCA_VPD_COLLECTED))
		return true;

	return false;
}

void vpd_data_parse(struct dt_node *node, const void *fruvpd, u32 fruvpd_sz)
{
	if (vpd_find_record(fruvpd, fruvpd_sz, "OPFR", NULL))
		vpd_opfr_parse(node, fruvpd, fruvpd_sz);
	else
		vpd_vini_parse(node, fruvpd, fruvpd_sz);
}

/* Create the /vpd node and add its children */
void dt_init_vpd_node(void)
{
	const char *name, *p_name;
	int count, index;
	uint64_t addr, p_addr;
	struct dt_node *dt_vpd;
	struct HDIF_common_hdr *slca_hdr;
	struct dt_node *parent, *node;
	const struct slca_entry *entry, *p_entry;

	dt_vpd = dt_new(dt_root, "vpd");
	assert(dt_vpd);
	dt_add_property_string(dt_vpd, "compatible", "ibm,opal-v3-vpd");

	slca_hdr = get_hdif(&spira.ntuples.slca, SLCA_HDIF_SIG);
	if (!slca_hdr) {
		prerror("SLCA Invalid\n");
		return;
	}

	count = HDIF_get_iarray_size(slca_hdr, SLCA_IDATA_ARRAY);
	if (count < 0) {
		prerror("SLCA: Can't find SLCA array size!\n");
		return;
	}

	for (index = 0; index < count; index++) {
		/* Get SLCA entry */
		entry = slca_get_entry(index);
		if (!entry)
			goto next_entry;

		/*
		 * A child entry is valid if all of the following criteria is met
		 *   a. SLCA_INSTALL_INSTALLED is set in s_entry->install_indic
		 *   b. SLCA_VPD_COLLECTED is set in s_entry->vpd_collected
		 *   c. The SLCA is not a duplicate entry.
		 */
		if (!valid_child_entry(entry))
			goto next_entry;

		name = vpd_map_name(entry->fru_id);
		addr = be16_to_cpu(entry->rsrc_id);
		/* Check node is already created or not */
		if (dt_find_by_name_addr(dt_vpd, name, addr))
			goto next_entry;

		/* Get parent node */
		if (index == SLCA_ROOT_INDEX) {
			parent = dt_vpd;
		} else {
			p_entry = slca_get_entry(be16_to_cpu(entry->parent_index));
			if (!p_entry)
				goto next_entry;
			p_name = vpd_map_name(p_entry->fru_id);
			p_addr = be16_to_cpu(p_entry->rsrc_id);
			parent = dt_find_by_name_addr(dt_vpd, p_name, p_addr);
		}
		if (!parent)
			goto next_entry;

		node = dt_new_addr(parent, name, addr);
		if (!node) {
			prerror("VPD: Creating node at %s@%"PRIx64" failed\n",
				name, addr);
			goto next_entry;
		}

		/* Add location code */
		slca_vpd_add_loc_code(node, be16_to_cpu(entry->my_index));
		/* Add FRU label */
		dt_add_property(node, "fru-type", entry->fru_id, 2);

next_entry:
		/* Skip dups -- dups are contiguous */
		index += entry->nr_dups;
	}
}

struct dt_node *dt_add_vpd_node(const struct HDIF_common_hdr *hdr,
				int indx_fru, int indx_vpd)
{
	const struct spira_fru_id *fru_id;
	unsigned int fruvpd_sz, fru_id_sz;
	const struct slca_entry *entry;
	struct dt_node *dt_vpd, *node;
	const void *fruvpd;
	const char *name;
	uint64_t addr;

	fru_id = HDIF_get_idata(hdr, indx_fru, &fru_id_sz);
	if (!fru_id)
		return NULL;

	fruvpd = HDIF_get_idata(hdr, indx_vpd, &fruvpd_sz);
	if (!CHECK_SPPTR(fruvpd))
		return NULL;

	dt_vpd = dt_find_by_path(dt_root, "/vpd");
	if (!dt_vpd)
		return NULL;

	entry = slca_get_entry(be16_to_cpu(fru_id->slca_index));
	if (!entry)
		return NULL;

	name = vpd_map_name(entry->fru_id);
	addr = be16_to_cpu(entry->rsrc_id);
	/* Get the node already created */
	node = dt_find_by_name_addr(dt_vpd, name, addr);
	/*
	 * It is unlikely that node not found because vpd nodes have the
	 * corresponding slca entry which we would have used to populate the vpd
	 * tree during the 'first' pass above so that we just need to perform
	 * VINI parse and add the vpd data..
	 */
	if (!node)
		return NULL;

	/* Parse VPD fields, ensure that it has not been added already */
	if (vpd_valid(fruvpd, fruvpd_sz)
	    && !dt_find_property(node, "ibm,vpd")) {
		dt_add_property(node, "ibm,vpd", fruvpd, fruvpd_sz);
		vpd_data_parse(node, fruvpd, fruvpd_sz);
	}

	return node;
}

static void dt_add_model_name(void)
{
	const char *model_name = NULL;
	const struct machine_info *mi;
	const struct iplparams_sysparams *p;
	const struct HDIF_common_hdr *iplp;
	const struct dt_property *model;

	model = dt_find_property(dt_root, "model");

	iplp = get_hdif(&spira.ntuples.ipl_parms, "IPLPMS");
	if (!iplp)
		goto def_model;

	p = HDIF_get_idata(iplp, IPLPARAMS_SYSPARAMS, NULL);
	if (!CHECK_SPPTR(p))
		goto def_model;

	if (be16_to_cpu(iplp->version) >= 0x60)
		model_name = p->sys_type_str;

def_model:
	if ((!model_name || model_name[0] == '\0') && model) {
		mi = machine_info_lookup(model->prop);
		if (mi)
			model_name = mi->name;
	}

	if(model_name)
		dt_add_property_string(dt_root, "model-name", model_name);
}

static void sysvpd_parse_opp(const void *sysvpd, unsigned int sysvpd_sz)
{
	const char *v;
	uint8_t sz;

	v = vpd_find(sysvpd, sysvpd_sz, "OSYS", "MM", &sz);
	if (v)
		dt_add_property_nstr(dt_root, "model", v, sz);
	else
		dt_add_property_string(dt_root, "model", "Unknown");

	v = vpd_find(sysvpd, sysvpd_sz, "OSYS", "SS", &sz);
	if (v)
		dt_add_property_nstr(dt_root, "system-id", v, sz);
	else
		dt_add_property_string(dt_root, "system-id", "Unknown");
}


static void sysvpd_parse_legacy(const void *sysvpd, unsigned int sysvpd_sz)
{
	const char *model;
	const char *system_id;
	const char *brand;
	uint8_t sz;

	model = vpd_find(sysvpd, sysvpd_sz, "VSYS", "TM", &sz);
	if (model)
		dt_add_property_nstr(dt_root, "model", model, sz);
	else
		dt_add_property_string(dt_root, "model", "Unknown");

	system_id = vpd_find(sysvpd, sysvpd_sz, "VSYS", "SE", &sz);
	if (system_id)
		dt_add_property_nstr(dt_root, "system-id", system_id, sz);
	else
		dt_add_property_string(dt_root, "system-id", "Unknown");

	brand = vpd_find(sysvpd, sysvpd_sz, "VSYS", "BR", &sz);
	if (brand)
		dt_add_property_nstr(dt_root, "system-brand", brand, sz);
	else
		dt_add_property_string(dt_root, "brand", "Unknown");
}

static void sysvpd_parse(void)
{
	const void *sysvpd;
	unsigned int sysvpd_sz;
	unsigned int fru_id_sz;
	struct dt_node *dt_vpd;
	const struct spira_fru_id *fru_id;
	struct HDIF_common_hdr *sysvpd_hdr;

	sysvpd_hdr = get_hdif(&spira.ntuples.system_vpd, SYSVPD_HDIF_SIG);
	if (!sysvpd_hdr)
		return;

	fru_id = HDIF_get_idata(sysvpd_hdr, SYSVPD_IDATA_FRU_ID, &fru_id_sz);
	if (!fru_id)
		return;

	sysvpd = HDIF_get_idata(sysvpd_hdr, SYSVPD_IDATA_KW_VPD, &sysvpd_sz);
	if (!CHECK_SPPTR(sysvpd))
		return;

	/* Add system VPD */
	dt_vpd = dt_find_by_path(dt_root, "/vpd");
	if (dt_vpd) {
		dt_add_property(dt_vpd, "ibm,vpd", sysvpd, sysvpd_sz);
		slca_vpd_add_loc_code(dt_vpd, be16_to_cpu(fru_id->slca_index));
	}

	/* Look for the new OpenPower "OSYS" first */
	if (vpd_find_record(sysvpd, sysvpd_sz, "OSYS", NULL))
		sysvpd_parse_opp(sysvpd, sysvpd_sz);
	else
		sysvpd_parse_legacy(sysvpd, sysvpd_sz);

	dt_add_model_name();
}

static void iokid_vpd_parse(const struct HDIF_common_hdr *iohub_hdr)
{
	const struct HDIF_child_ptr *iokids;
	const struct HDIF_common_hdr *iokid;
	unsigned int i;

	iokids = HDIF_child_arr(iohub_hdr, CECHUB_CHILD_IO_KIDS);
	if (!CHECK_SPPTR(iokids)) {
		prerror("VPD: No IOKID child array\n");
		return;
	}

	for (i = 0; i < be32_to_cpu(iokids->count); i++) {
		iokid = HDIF_child(iohub_hdr, iokids, i,
					IOKID_FRU_HDIF_SIG);
		/* IO KID VPD structure layout is similar to FRUVPD */
		if (iokid)
			dt_add_vpd_node(iokid,
				FRUVPD_IDATA_FRU_ID, FRUVPD_IDATA_KW_VPD);
	}
}

static void iohub_vpd_parse(void)
{
	const struct HDIF_common_hdr *iohub_hdr;
	const struct cechub_hub_fru_id *fru_id_data;
	unsigned int i, vpd_sz, fru_id_sz;

	if (!get_hdif(&spira.ntuples.cec_iohub_fru, CECHUB_FRU_HDIF_SIG)) {
		prerror("VPD: Could not find IO HUB FRU data\n");
		return;
	}

	for_each_ntuple_idx(&spira.ntuples.cec_iohub_fru, iohub_hdr,
					i, CECHUB_FRU_HDIF_SIG) {

		fru_id_data = HDIF_get_idata(iohub_hdr,
					     CECHUB_FRU_ID_DATA_AREA,
					     &fru_id_sz);

		/* P8, IO HUB is on processor card and we have a
		 * daughter card array
		 */
		if (fru_id_data &&
		    be32_to_cpu(fru_id_data->card_type) == CECHUB_FRU_TYPE_CPU_CARD) {
			iokid_vpd_parse(iohub_hdr);
			continue;
		}

		/* On P7, the keyword VPD will not be NULL */
		if (HDIF_get_idata(iohub_hdr,
				   CECHUB_ASCII_KEYWORD_VPD, &vpd_sz))
			dt_add_vpd_node(iohub_hdr, CECHUB_FRU_ID_DATA,
					CECHUB_ASCII_KEYWORD_VPD);
	}
}

static void _vpd_parse(struct spira_ntuple tuple)
{
	const struct HDIF_common_hdr *fruvpd_hdr;
	unsigned int i;

	if (!get_hdif(&tuple, FRUVPD_HDIF_SIG))
		return;

	for_each_ntuple_idx(&tuple, fruvpd_hdr, i, FRUVPD_HDIF_SIG)
		dt_add_vpd_node(fruvpd_hdr,
				FRUVPD_IDATA_FRU_ID, FRUVPD_IDATA_KW_VPD);
}

void vpd_parse(void)
{
	const struct HDIF_common_hdr *fruvpd_hdr;

	/* System VPD uses the VSYS record, so its special */
	sysvpd_parse();

	/* Enclosure */
	_vpd_parse(spira.ntuples.nt_enclosure_vpd);

	/* Backplane */
	_vpd_parse(spira.ntuples.backplane_vpd);

	/* clock card -- does this use the FRUVPD sig? */
	_vpd_parse(spira.ntuples.clock_vpd);

	/* Anchor card */
	_vpd_parse(spira.ntuples.anchor_vpd);

	/* Op panel -- does this use the FRUVPD sig? */
	_vpd_parse(spira.ntuples.op_panel_vpd);

	/* External cache FRU vpd -- does this use the FRUVPD sig? */
	_vpd_parse(spira.ntuples.ext_cache_fru_vpd);

	/* Misc CEC FRU */
	_vpd_parse(spira.ntuples.misc_cec_fru_vpd);

	/* CEC IO HUB FRU */
	iohub_vpd_parse();

	/*
	 * SP subsystem -- while the rest of the SPINFO structure is
	 * different, the FRU ID data and pointer pair to keyword VPD
	 * are the same offset as a FRUVPD entry. So reuse it
	 */
	fruvpd_hdr = get_hdif(&spira.ntuples.sp_subsys, SPSS_HDIF_SIG);
	if (fruvpd_hdr)
		dt_add_vpd_node(fruvpd_hdr,
				FRUVPD_IDATA_FRU_ID, FRUVPD_IDATA_KW_VPD);
}
