/* Copyright 2013-2014 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.
 */
#define pr_fmt(fmt) "LXVPD: " fmt

#include <skiboot.h>
#include <device.h>
#include <vpd.h>
#include <pci.h>
#include <pci-cfg.h>
#include <pci-slot.h>

#include "lxvpd.h"

/*
 * Currently, the lxvpd PCI slot struct is shared by multiple
 * platforms (Apollo and Firenze), but each slot still has
 * platform specific features. In order for unified data structs,
 * "struct lxvpd_slot" is expected to be embedded in platform
 * PCI slot struct. "entry_size" indicates the size of platform
 * specific PCI slot instance.
 */
struct lxvpd_pci_slot_data {
	uint8_t		num_slots;
	int32_t		entry_size;	/* Size of platform PCI slot  */
	void		*slots;		/* Data of platform PCI slots */
};

static bool lxvpd_supported_slot(struct phb *phb, struct pci_device *pd)
{
	/* PHB should always be valid */
	if (!phb)
		return false;

	/* We expect platform slot for root complex */
	if (!pd)
		return true;

	/* We support the root complex at the top level */
	if (pd->dev_type == PCIE_TYPE_ROOT_PORT && !pd->parent)
		return true;

	/* We support an upstream switch port below the root complex */
	if (pd->dev_type == PCIE_TYPE_SWITCH_UPPORT &&
	    pd->parent && pd->parent->dev_type == PCIE_TYPE_ROOT_PORT &&
	    !pd->parent->parent)
		return true;

	/* We support a downstream switch port below an upstream port
	 * below the root complex
	 */
	if (pd->dev_type == PCIE_TYPE_SWITCH_DNPORT &&
	    pd->parent && pd->parent->dev_type == PCIE_TYPE_SWITCH_UPPORT &&
	    pd->parent->parent &&
	    pd->parent->parent->dev_type == PCIE_TYPE_ROOT_PORT &&
	    !pd->parent->parent->parent)
		return true;

	/* Anything else, bail */
	return false;
}

void *lxvpd_get_slot(struct pci_slot *slot)
{
	struct phb *phb = slot->phb;
	struct pci_device *pd = slot->pd;
	struct lxvpd_pci_slot_data *sdata = phb->platform_data;
	struct lxvpd_pci_slot *s = NULL;
	uint8_t slot_num = pd ? ((pd->bdfn >> 3) & 0x1f) : 0xff;
	bool is_phb = (pd && pd->parent) ? false : true;
	uint8_t index;

	/* Check if we have slot info */
	if (!sdata) {
		prlog(PR_DEBUG, "PHB%04x not have VPD data\n",
			  phb->opal_id);
		return NULL;
	}

	/* Platform slot attached ? */
	s = slot->data;
	if (s) {
		prlog(PR_DEBUG, "Slot %016llx had platform data [%s]\n",
			  slot->id, s->label);
		return s;
	}

	/*
	 * This code only handles PHBs and PCIe switches at the
	 * top level. We do not handle any other switch nor any
	 * other type of PCI/PCI-X bridge. Generally, we have
	 * more strict rules to support slot than PCI core.
	 */
	if (!lxvpd_supported_slot(phb, pd)) {
		prlog(PR_DEBUG, "Slot %016llx not supported\n",
			  slot->id);
		return NULL;
	}

	/* Iterate the platform slot array */
	for (index = 0; index < sdata->num_slots; index++) {
		s = sdata->slots + (index * sdata->entry_size);

		/* Match PHB with switch_id == 0 */
		if (is_phb && s->switch_id == 0) {
			slot->data = s;
			s->pci_slot = slot;
			prlog(PR_DEBUG, "Found [%s] for PHB slot %016llx\n",
				  s->label, slot->id);

			return s;
		}

		/* Match switch port with switch_id != 0 */
		if (!is_phb && s->switch_id != 0 && s->dev_id == slot_num) {
			slot->data = s;
			s->pci_slot = slot;
			prlog(PR_DEBUG, "Found [%s] for slot %016llx\n",
				  s->label, slot->id);

			return s;
		}
	}

	prlog(PR_DEBUG, "No data found for %sslot %016llx\n",
		  is_phb ? "PHB " : " ", slot->id);
	return NULL;
}

void lxvpd_extract_info(struct pci_slot *slot, struct lxvpd_pci_slot *s)
{
	slot->pluggable      = s->pluggable ? 1 : 0;
	slot->power_ctl      = s->power_ctl ? 1 : 0;
	slot->power_led_ctl  = s->pwr_led_ctl;
	slot->attn_led_ctl   = s->attn_led_ctl;
	slot->connector_type = s->connector_type;
	slot->card_desc      = s->card_desc;
	slot->card_mech      = s->card_mech;
	slot->wired_lanes    = s->wired_lanes;
}

static struct lxvpd_pci_slot_data *lxvpd_alloc_slots(struct phb *phb,
						     uint8_t count,
						     uint32_t slot_size)
{
	struct lxvpd_pci_slot_data *sdata;

	sdata = zalloc(sizeof(struct lxvpd_pci_slot_data) + count * slot_size);
	assert(sdata);
	sdata->num_slots   = count;
	sdata->entry_size  = slot_size;
	sdata->slots       = sdata + 1;
	phb->platform_data = sdata;

	return sdata;
}

static void lxvpd_format_label(char *dst, const char *src, size_t len)
{
	int i;

	memcpy(dst, src, len);

	/* Remove blank suffix */
	for (i = strlen(dst) - 1; i >= 0; i--) {
		if (dst[i] != ' ')
			break;

		dst[i] = 0;
	}
}

static void lxvpd_parse_1004_map(struct phb *phb,
				 const uint8_t *sm,
				 uint8_t size,
				 uint32_t slot_size)
{
	struct lxvpd_pci_slot_data *sdata;
	struct lxvpd_pci_slot *s;
	const struct pci_slot_entry_1004 *entry;
	uint8_t num_slots, slot;

	num_slots = (size / sizeof(struct pci_slot_entry_1004));
	sdata = lxvpd_alloc_slots(phb, num_slots, slot_size);

	/* Iterate through the entries in the keyword */
	entry = (const struct pci_slot_entry_1004 *)sm;
	for (slot = 0; slot < num_slots; slot++, entry++) {
		s = sdata->slots + slot * sdata->entry_size;

		/* Figure out PCI slot info */
		lxvpd_format_label(s->label, entry->label, 3);
		s->slot_index     = entry->slot_index;
		s->switch_id      = entry->pba >> 4;
		s->vswitch_id     = entry->pba & 0xf;
		s->dev_id         = entry->sba;
		s->pluggable      = ((entry->p0.byte & 0x20) == 0);
		s->power_ctl      = !!(entry->p0.byte & 0x40);
		s->bus_clock      = entry->p2.bus_clock - 4;
		s->connector_type = entry->p2.connector_type - 5;
		s->card_desc      = entry->p3.byte >> 6;
		if (entry->p3.byte < 0xc0)
			s->card_desc -= 4;
		s->card_mech      = (entry->p3.byte >> 4) & 0x3;
		s->pwr_led_ctl    = (entry->p3.byte & 0xf) >> 2;
		s->attn_led_ctl   = entry->p3.byte & 0x3;

		switch(entry->p1.wired_lanes) {
		case 1: s->wired_lanes = PCI_SLOT_WIRED_LANES_PCIX_32;  break;
		case 2: /* fall through */
		case 3: s->wired_lanes = PCI_SLOT_WIRED_LANES_PCIX_64;  break;
		case 4: s->wired_lanes = PCI_SLOT_WIRED_LANES_PCIE_X1;  break;
		case 5: s->wired_lanes = PCI_SLOT_WIRED_LANES_PCIE_X4;  break;
		case 6: s->wired_lanes = PCI_SLOT_WIRED_LANES_PCIE_X8;  break;
		case 7: s->wired_lanes = PCI_SLOT_WIRED_LANES_PCIE_X16; break;
		default:
			s->wired_lanes = PCI_SLOT_WIRED_LANES_UNKNOWN;
		}

		prlog(PR_DEBUG, "1004 Platform data [%s] %02x %02x on PHB%04x\n",
			  s->label, s->switch_id, s->dev_id, phb->opal_id);
	}
}

static void lxvpd_parse_1005_map(struct phb *phb,
				 const uint8_t *sm,
				 uint8_t size,
				 uint32_t slot_size)
{
	struct lxvpd_pci_slot_data *sdata;
	struct lxvpd_pci_slot *s;
	const struct pci_slot_entry_1005 *entry;
	uint8_t num_slots, slot;

	num_slots = (size / sizeof(struct pci_slot_entry_1005));
	sdata = lxvpd_alloc_slots(phb, num_slots, slot_size);

	/* Iterate through the entries in the keyword */
	entry = (const struct pci_slot_entry_1005 *)sm;
	for (slot = 0; slot < num_slots; slot++, entry++) {
		s = sdata->slots + slot * sdata->entry_size;

		/* Put slot info into pci device structure */
		lxvpd_format_label(s->label, entry->label, 8);
		s->slot_index     = entry->slot_index;
		s->switch_id      = entry->pba >> 4;
		s->vswitch_id     = entry->pba & 0xf;
		s->dev_id         = entry->switch_device_id;
		s->pluggable      = (entry->p0.pluggable == 0);
		s->power_ctl      = entry->p0.power_ctl;
		s->bus_clock      = entry->p2.bus_clock;
		s->connector_type = entry->p2.connector_type;
		s->card_desc      = entry->p3.byte >> 6;
		s->card_mech      = (entry->p3.byte >> 4) & 0x3;
		s->pwr_led_ctl    = (entry->p3.byte & 0xf) >> 2;
		s->attn_led_ctl   = entry->p3.byte & 0x3;
		s->wired_lanes    = entry->p1.wired_lanes;
		if (s->wired_lanes > PCI_SLOT_WIRED_LANES_PCIE_X32)
			s->wired_lanes = PCI_SLOT_WIRED_LANES_UNKNOWN;

		prlog(PR_DEBUG, "1005 Platform data [%s] %02x %02x on PHB%04x\n",
			  s->label, s->switch_id, s->dev_id, phb->opal_id);
	}
}

void lxvpd_process_slot_entries(struct phb *phb,
				struct dt_node *node,
				uint8_t chip_id,
				uint8_t index,
				uint32_t slot_size)
{
	const void *lxvpd;
	const uint8_t *pr_rec, *pr_end, *sm;
	size_t lxvpd_size, pr_size;
	const uint16_t *mf = NULL;
	char record[5] = "PR00";
	uint8_t mf_sz, sm_sz;
	bool found = false;

	record[2] += chip_id;
	record[3] += index;
	record[4] = 0;

	/* Get LX VPD pointer */
	lxvpd = dt_prop_get_def_size(node, "ibm,io-vpd", NULL, &lxvpd_size);
	if (!lxvpd) {
		prlog(PR_WARNING, "No data found for PHB%04x %s\n",
			   phb->opal_id, record);
		return;
	}

	pr_rec = vpd_find_record(lxvpd, lxvpd_size, record, &pr_size);
	if (!pr_rec) {
		prlog(PR_WARNING, "Record %s not found on PHB%04x\n",
			   record, phb->opal_id);
		return;
	}

	/* As long as there's still something in the PRxy record */
	prlog(PR_DEBUG, "PHB%04x record %s has %ld bytes\n",
		  phb->opal_id, record, pr_size);
	pr_end = pr_rec + pr_size;
	while (pr_rec < pr_end) {
		pr_size = pr_end - pr_rec;

		/* Find the next MF keyword */
		mf = vpd_find_keyword(pr_rec, pr_size, "MF", &mf_sz);
		/* And the corresponding SM */
		sm = vpd_find_keyword(pr_rec, pr_size, "SM", &sm_sz);
		if (!mf || !sm) {
			if (!found)
				prlog(PR_WARNING, "Slot Map keyword %s not found\n",
					   record);
			return;
		}

		prlog(PR_DEBUG, "Found 0x%04x map...\n", *mf);
		switch (*mf) {
		case 0x1004:
			lxvpd_parse_1004_map(phb, sm + 1, sm_sz - 1, slot_size);
			found = true;
			break;
		case 0x1005:
			lxvpd_parse_1005_map(phb, sm + 1, sm_sz - 1, slot_size);
			found = true;
			break;
			/* Add support for 0x1006 maps ... */
		}

		pr_rec = sm + sm_sz;
	}
}

void lxvpd_add_slot_properties(struct pci_slot *slot,
			       struct dt_node *np)
{
	struct phb *phb = slot->phb;
	struct lxvpd_pci_slot *s = slot->data;
	char loc_code[LOC_CODE_SIZE];
	size_t base_loc_code_len, slot_label_len;

	/* Check if we have platform specific slot */
	if (!s || !np)
		return;

	/* Check PHB base location code */
	if (!phb->base_loc_code)
		return;

	/* Check location length is valid */
	base_loc_code_len = strlen(phb->base_loc_code);
	slot_label_len = strlen(s->label);
	if ((base_loc_code_len + slot_label_len + 1) >= LOC_CODE_SIZE)
		return;

	/* Location code */
	strcpy(loc_code, phb->base_loc_code);
	strcat(loc_code, "-");
	strcat(loc_code, s->label);
	dt_add_property(np, "ibm,slot-location-code",
			loc_code, strlen(loc_code) + 1);
	dt_add_property_string(np, "ibm,slot-label",
			       s->label);
}
